diff --git a/.circleci/config.yml b/.circleci/config.yml index dcf2ba804c6..96bd056adfb 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -8,7 +8,7 @@ aliases: docker: # specify the version you desire here - image: cimg/node:20.14.0-browsers - resource_class: xlarge + resource_class: small # Specify service dependencies here if necessary # CircleCI maintains a library of pre-built images # documented at https://circleci.com/docs/2.0/circleci-images/ diff --git a/browserstack.err b/browserstack.err new file mode 100644 index 00000000000..a6e141486fb --- /dev/null +++ b/browserstack.err @@ -0,0 +1 @@ +[object Object] \ No newline at end of file diff --git a/local.log b/local.log new file mode 100644 index 00000000000..8b137891791 --- /dev/null +++ b/local.log @@ -0,0 +1 @@ + diff --git a/logs/bstack-wdio-service.log b/logs/bstack-wdio-service.log new file mode 100644 index 00000000000..eae4c30a34b --- /dev/null +++ b/logs/bstack-wdio-service.log @@ -0,0 +1,4 @@ +[90m2024-11-08T15:07:27.246Z[39m [32mDEBUG[39m [97m@wdio/browserstack-service[39m username/accesskey not passed +[90m2024-11-08T15:07:27.247Z[39m [32mDEBUG[39m [97m@wdio/browserstack-service[39m app is not defined in browserstack-service config, skipping ... +[90m2024-11-08T15:07:27.247Z[39m [32mDEBUG[39m [97m@wdio/browserstack-service[39m Sending launch start event +[90m2024-11-08T15:07:27.727Z[39m [32mDEBUG[39m [97m@wdio/browserstack-service[39m [Start_Build] Success response: {"errorTraceId":"ad34fe60-15bb-4729-a60c-0828bb96c20a","message":"Access to BrowserStack denied due to incorrect credentials.","errorType":"ERROR_INVALID_CREDENTIALS"} diff --git a/modules/adverxoBidAdapter.js b/modules/adverxoBidAdapter.js new file mode 100644 index 00000000000..f7fcff3858c --- /dev/null +++ b/modules/adverxoBidAdapter.js @@ -0,0 +1,354 @@ +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} +]; + +const AUCTION_URLS = { + adverxo: 'js.pbsadverxo.com', + adport: 'diclotrans.com', + bidsmind: 'egrevirda.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/kargoBidAdapter.js b/modules/kargoBidAdapter.js deleted file mode 100644 index 46ad7dfec71..00000000000 --- a/modules/kargoBidAdapter.js +++ /dev/null @@ -1,549 +0,0 @@ -import { _each, isEmpty, buildUrl, deepAccess, pick, logError } from '../src/utils.js'; -import { config } from '../src/config.js'; -import { registerBidder } from '../src/adapters/bidderFactory.js'; -import { getStorageManager } from '../src/storageManager.js'; -import { BANNER, VIDEO } from '../src/mediaTypes.js'; - -const PREBID_VERSION = '$prebid.version$' - -const BIDDER = Object.freeze({ - CODE: 'kargo', - HOST: 'krk2.kargo.com', - REQUEST_METHOD: 'POST', - REQUEST_ENDPOINT: '/api/v1/prebid', - TIMEOUT_ENDPOINT: '/api/v1/event/timeout', - GVLID: 972, - SUPPORTED_MEDIA_TYPES: [BANNER, VIDEO], -}); - -const STORAGE = getStorageManager({bidderCode: BIDDER.CODE}); - -const CURRENCY = Object.freeze({ - KEY: 'currency', - US_DOLLAR: 'USD', -}); - -const REQUEST_KEYS = Object.freeze({ - USER_DATA: 'ortb2.user.data', - SOCIAL_CANVAS: 'params.socialCanvas', - SUA: 'ortb2.device.sua', - TDID_ADAPTER: 'userId.tdid', -}); - -const SUA = Object.freeze({ - BROWSERS: 'browsers', - MOBILE: 'mobile', - MODEL: 'model', - PLATFORM: 'platform', - SOURCE: 'source', -}); - -const SUA_ATTRIBUTES = [ - SUA.BROWSERS, - SUA.MOBILE, - SUA.MODEL, - SUA.SOURCE, - SUA.PLATFORM, -]; - -const CERBERUS = Object.freeze({ - KEY: 'krg_crb', - 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', - PAGE_VIEW_URL: 'pageViewUrl' -}); - -let sessionId, - lastPageUrl, - requestCounter; - -function isBidRequestValid(bid) { - if (!bid || !bid.params) { - return false; - } - - return !!bid.params.placementId; -} - -function buildRequests(validBidRequests, bidderRequest) { - const currencyObj = config.getConfig(CURRENCY.KEY); - const currency = (currencyObj && currencyObj.adServerCurrency) ? currencyObj.adServerCurrency : null; - const impressions = []; - - _each(validBidRequests, bid => { - impressions.push(getImpression(bid)) - }); - - const firstBidRequest = validBidRequests[0]; - const tdidAdapter = deepAccess(firstBidRequest, REQUEST_KEYS.TDID_ADAPTER); - - const metadata = getAllMetadata(bidderRequest); - - const krakenParams = Object.assign({}, { - pbv: PREBID_VERSION, - aid: firstBidRequest.auctionId, - sid: _getSessionId(), - url: metadata.pageURL, - timeout: bidderRequest.timeout, - ts: new Date().getTime(), - device: { - size: [ - window.screen.width, - window.screen.height - ] - }, - imp: impressions, - user: getUserIds(tdidAdapter, bidderRequest.uspConsent, bidderRequest.gdprConsent, firstBidRequest.userIdAsEids, bidderRequest.gppConsent), - ext: getExtensions(firstBidRequest.ortb2, bidderRequest?.refererInfo) - }); - - // Add site.cat if it exists - if (firstBidRequest.ortb2?.site?.cat != null) { - krakenParams.site = { cat: firstBidRequest.ortb2.site.cat }; - } - - // Add schain - if (firstBidRequest.schain && firstBidRequest.schain.nodes) { - krakenParams.schain = firstBidRequest.schain - } - - // Add user data object if available - krakenParams.user.data = deepAccess(firstBidRequest, REQUEST_KEYS.USER_DATA) || []; - - const reqCount = getRequestCount() - if (reqCount != null) { - krakenParams.requestCount = reqCount; - } - - // Add currency if not USD - if (currency != null && currency != CURRENCY.US_DOLLAR) { - krakenParams.cur = currency; - } - - if (metadata.rawCRB != null) { - krakenParams.rawCRB = metadata.rawCRB - } - - if (metadata.rawCRBLocalStorage != null) { - krakenParams.rawCRBLocalStorage = metadata.rawCRBLocalStorage - } - - // Pull Social Canvas segments and embed URL - const socialCanvas = deepAccess(firstBidRequest, REQUEST_KEYS.SOCIAL_CANVAS); - - if (socialCanvas != null) { - krakenParams.socan = socialCanvas; - } - - // User Agent Client Hints / SUA - const uaClientHints = deepAccess(firstBidRequest, REQUEST_KEYS.SUA); - if (uaClientHints) { - const suaValidAttributes = [] - - SUA_ATTRIBUTES.forEach(suaKey => { - const suaValue = uaClientHints[suaKey]; - if (!suaValue) { - return; - } - - // Do not pass any empty strings - if (typeof suaValue == 'string' && suaValue.trim() === '') { - return; - } - - switch (suaKey) { - case SUA.MOBILE && suaValue < 1: // Do not pass 0 value for mobile - case SUA.SOURCE && suaValue < 1: // Do not pass 0 value for source - break; - default: - suaValidAttributes.push(suaKey); - } - }); - - krakenParams.device.sua = pick(uaClientHints, suaValidAttributes); - } - - const validPageId = getLocalStorageSafely(CERBERUS.PAGE_VIEW_ID) != null - const validPageTimestamp = getLocalStorageSafely(CERBERUS.PAGE_VIEW_TIMESTAMP) != null - const validPageUrl = getLocalStorageSafely(CERBERUS.PAGE_VIEW_URL) != null - - const page = {} - if (validPageId) { - page.id = getLocalStorageSafely(CERBERUS.PAGE_VIEW_ID); - } - if (validPageTimestamp) { - page.timestamp = Number(getLocalStorageSafely(CERBERUS.PAGE_VIEW_TIMESTAMP)); - } - if (validPageUrl) { - page.url = getLocalStorageSafely(CERBERUS.PAGE_VIEW_URL); - } - if (!isEmpty(page)) { - 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}`, - data: krakenParams, - currency: currency - }); -} - -function interpretResponse(response, bidRequest) { - const bids = response.body; - const fledgeAuctionConfigs = []; - const bidResponses = []; - - if (isEmpty(bids) || typeof bids !== 'object') { - return bidResponses; - } - - for (const [bidID, adUnit] of Object.entries(bids)) { - let meta = { - mediaType: adUnit.mediaType && BIDDER.SUPPORTED_MEDIA_TYPES.includes(adUnit.mediaType) ? adUnit.mediaType : BANNER - }; - - if (adUnit.metadata?.landingPageDomain) { - meta.clickUrl = adUnit.metadata.landingPageDomain[0]; - meta.advertiserDomains = adUnit.metadata.landingPageDomain; - } - - const bidResponse = { - requestId: bidID, - cpm: Number(adUnit.cpm), - width: adUnit.width, - height: adUnit.height, - ttl: 300, - creativeId: adUnit.creativeID, - dealId: adUnit.targetingCustom, - netRevenue: true, - currency: adUnit.currency || bidRequest.currency, - mediaType: meta.mediaType, - meta: meta - }; - - if (meta.mediaType == VIDEO) { - if (adUnit.admUrl) { - bidResponse.vastUrl = adUnit.admUrl; - } else { - bidResponse.vastXml = adUnit.adm; - } - } else { - bidResponse.ad = adUnit.adm; - } - - bidResponses.push(bidResponse); - - if (adUnit.auctionConfig) { - fledgeAuctionConfigs.push({ - bidId: bidID, - config: adUnit.auctionConfig - }) - } - } - - if (fledgeAuctionConfigs.length > 0) { - return { - bids: bidResponses, - paapi: fledgeAuctionConfigs - } - } else { - return bidResponses; - } -} - -function getUserSyncs(syncOptions, _, gdprConsent, usPrivacy, gppConsent) { - const syncs = []; - const seed = _generateRandomUUID(); - const clientId = getClientId(); - - var gdpr = (gdprConsent && gdprConsent.gdprApplies) ? 1 : 0; - var gdprConsentString = (gdprConsent && gdprConsent.consentString) ? gdprConsent.consentString : ''; - - var gppString = (gppConsent && gppConsent.consentString) ? gppConsent.consentString : ''; - var gppApplicableSections = (gppConsent && gppConsent.applicableSections && Array.isArray(gppConsent.applicableSections)) ? gppConsent.applicableSections.join(',') : ''; - - // don't sync if opted out via usPrivacy - if (typeof usPrivacy == 'string' && usPrivacy.length == 4 && usPrivacy[0] == 1 && usPrivacy[2] == 'Y') { - return syncs; - } - if (syncOptions.iframeEnabled && seed && clientId) { - 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; -} - -function onTimeout(timeoutData) { - if (timeoutData == null) { - return; - } - - timeoutData.forEach((bid) => { - sendTimeoutData(bid.auctionId, bid.timeout); - }); -} - -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 - var buffer = new Uint8Array(16); - crypto.getRandomValues(buffer); - buffer[6] = (buffer[6] & ~176) | 64; - buffer[8] = (buffer[8] & ~64) | 128; - var hex = Array.prototype.map.call(new Uint8Array(buffer), function(x) { - return ('00' + x.toString(16)).slice(-2); - }).join(''); - return hex.slice(0, 8) + '-' + hex.slice(8, 12) + '-' + hex.slice(12, 16) + '-' + hex.slice(16, 20) + '-' + hex.slice(20); - } catch (e) { - return ''; - } -} - -function _getCrb() { - let localStorageCrb = getCrbFromLocalStorage(); - if (Object.keys(localStorageCrb).length) { - return localStorageCrb; - } - return getCrbFromCookie(); -} - -function _getSessionId() { - if (!sessionId) { - sessionId = _generateRandomUUID(); - } - return sessionId; -} - -function getCrbFromCookie() { - try { - const crb = JSON.parse(STORAGE.getCookie(CERBERUS.KEY)); - if (crb && crb.v) { - let vParsed = JSON.parse(atob(crb.v)); - if (vParsed) { - return vParsed; - } - } - return {}; - } catch (e) { - return {}; - } -} - -function getCrbFromLocalStorage() { - try { - return JSON.parse(atob(getLocalStorageSafely(CERBERUS.KEY))); - } catch (e) { - return {}; - } -} - -function getLocalStorageSafely(key) { - try { - return STORAGE.getDataFromLocalStorage(key); - } catch (e) { - return null; - } -} - -function getUserIds(tdidAdapter, usp, gdpr, eids, gpp) { - const crb = spec._getCrb(); - const userIds = { - crbIDs: crb.syncIds || {} - }; - - // Pull Trade Desk ID - if (!tdidAdapter && crb.tdID) { - userIds.tdID = crb.tdID; - } else if (tdidAdapter) { - userIds.tdID = tdidAdapter; - } - - // USP - if (usp) { - userIds.usp = usp; - } - - // GDPR - if (gdpr) { - userIds.gdpr = { - consent: gdpr.consentString || '', - applies: !!gdpr.gdprApplies, - }; - } - - // Kargo ID - if (crb.lexId != null) { - userIds.kargoID = crb.lexId; - } - - // Client ID - if (crb.clientId != null) { - userIds.clientID = crb.clientId; - } - - // Opt Out - if (crb.optOut != null) { - userIds.optOut = crb.optOut; - } - - // User ID Sub-Modules (userIdAsEids) - if (eids != null) { - userIds.sharedIDEids = eids; - } - - // GPP - if (gpp) { - const parsedGPP = {}; - if (gpp.consentString) { - parsedGPP.gppString = gpp.consentString; - } - if (gpp.applicableSections) { - parsedGPP.applicableSections = gpp.applicableSections; - } - if (!isEmpty(parsedGPP)) { - userIds.gpp = parsedGPP; - } - } - - return userIds; -} - -function getClientId() { - const crb = spec._getCrb(); - return crb.clientId; -} - -function getAllMetadata(bidderRequest) { - return { - pageURL: bidderRequest?.refererInfo?.page, - rawCRB: STORAGE.getCookie(CERBERUS.KEY), - rawCRBLocalStorage: getLocalStorageSafely(CERBERUS.KEY) - }; -} - -function getRequestCount() { - if (lastPageUrl === window.location.pathname) { - return ++requestCounter; - } - lastPageUrl = window.location.pathname; - return requestCounter = 0; -} - -function sendTimeoutData(auctionId, auctionTimeout) { - const params = { aid: auctionId, ato: auctionTimeout }; - const timeoutRequestUrl = buildUrl({ - protocol: 'https', - hostname: BIDDER.HOST, - pathname: BIDDER.TIMEOUT_ENDPOINT, - search: params, - }); - - fetch(timeoutRequestUrl, { - method: 'GET', - keepalive: true, - }).catch((e) => { - logError('Kargo: sendTimeoutData/fetch threw an error: ', e); - }); -} - -function getImpression(bid) { - const imp = { - id: bid.bidId, - tid: bid.ortb2Imp?.ext?.tid, - pid: bid.params.placementId, - code: bid.adUnitCode - }; - - if (bid.bidRequestsCount > 0) { - imp.bidRequestCount = bid.bidRequestsCount; - } - - if (bid.bidderRequestsCount > 0) { - imp.bidderRequestCount = bid.bidderRequestsCount; - } - - if (bid.bidderWinsCount > 0) { - imp.bidderWinCount = bid.bidderWinsCount; - } - - const gpid = deepAccess(bid, 'ortb2Imp.ext.gpid') || deepAccess(bid, 'ortb2Imp.ext.data.pbadslot'); - if (gpid) { - imp.fpd = { - gpid: gpid - } - } - - // Add full ortb2Imp object as backup - if (bid.ortb2Imp) { - imp.ext = { ortb2Imp: bid.ortb2Imp }; - } - - if (bid.mediaTypes) { - const { banner, video, native } = bid.mediaTypes; - - if (banner) { - imp.banner = banner; - } - - if (video) { - imp.video = video; - } - - if (native) { - imp.native = native; - } - - if (typeof bid.getFloor === 'function') { - let floorInfo; - try { - floorInfo = bid.getFloor({ - currency: 'USD', - mediaType: '*', - size: '*' - }); - } catch (e) { - logError('Kargo: getFloor threw an error: ', e); - } - imp.floor = typeof floorInfo === 'object' && floorInfo.currency === 'USD' && !isNaN(parseInt(floorInfo.floor)) ? floorInfo.floor : undefined; - } - } - - return imp -} - -export const spec = { - gvlid: BIDDER.GVLID, - code: BIDDER.CODE, - isBidRequestValid, - buildRequests, - interpretResponse, - getUserSyncs, - supportedMediaTypes: BIDDER.SUPPORTED_MEDIA_TYPES, - onTimeout, - _getCrb, - _getSessionId -}; - -registerBidder(spec); diff --git a/test/spec/modules/adverxoBidAdapter_spec.js b/test/spec/modules/adverxoBidAdapter_spec.js new file mode 100644 index 00000000000..17ea33f8e67 --- /dev/null +++ b/test/spec/modules/adverxoBidAdapter_spec.js @@ -0,0 +1,724 @@ +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', + 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: '