From 8c72bc2239e3f9adbb14b40ec211b2be8eb8a0fb Mon Sep 17 00:00:00 2001 From: Giuseppe Cera <117671343+giuseppe-exads@users.noreply.github.com> Date: Thu, 2 May 2024 14:31:04 +0100 Subject: [PATCH] EXADS Bid Adapter: initial release (#11284) * First commit * fix: readme.md * fix: changed exads urls * fix: Tools and suggestions related to the doc * fix: from code review * fix: from code review * fix: from code review * fix: error from code review - native example * fox: from code review * fix: from code review * fix: from code review * fix: native img set as mandatory * fix: from code review * fix: from code review * fix: from code review * fix: from code review * fix: from code review * fix: from code review * fix: bidfloor and bidfloorcur set as optional * fix: dsa * fix: mananing multiple responses * fix: unit test after code review * fix: fixing native snippet code * fix: from code review * fix: video events after code review * fix: video module into documentation * fix: impression tracker for native * fix: afeter code review * fix: unit tests * fix: added badv and bcat * fix: video -> mimes and protocols * fix * fix: removed image_output and video_output params, forcing always html for rtb banner * fix: gulp * fix: added site.name * fix: removed EXADS dir * fix: after linting * fix: unit tests * fix: final dsa solution * fix: dsa * fix: fix instream example * fix: doc media type context * fix: documented the endpoint param into native section * fix: related to markdown lint validation (#2) * fix: from CR (#3) --------- Co-authored-by: tfoliveira --- modules/exadsBidAdapter.js | 514 ++++++++++++++++++ modules/exadsBidAdapter.md | 484 +++++++++++++++++ test/spec/modules/exadsBidAdapter_spec.js | 632 ++++++++++++++++++++++ 3 files changed, 1630 insertions(+) create mode 100644 modules/exadsBidAdapter.js create mode 100644 modules/exadsBidAdapter.md create mode 100644 test/spec/modules/exadsBidAdapter_spec.js diff --git a/modules/exadsBidAdapter.js b/modules/exadsBidAdapter.js new file mode 100644 index 00000000000..31d75db470d --- /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 = 'exadsadserver'; + +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: '' + }; + + 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.userAgent.match(/Android/i)) { + envParams.osName = 'Android'; + } else if (envParams.userAgent.userAgent.match(/iPhone|iPad|iPod/i)) { + envParams.osName = 'iOS'; + } else if (envParams.userAgent.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..06b873d8da8 --- /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: 'exadsadserver', + 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: 'exadsadserver', + 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: 'exadsadserver', + 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: 'exadsadserver', + 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/test/spec/modules/exadsBidAdapter_spec.js b/test/spec/modules/exadsBidAdapter_spec.js new file mode 100644 index 00000000000..9253f21ddf1 --- /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 = 'exadsadserver'; + + 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'); + }); + }); +});