From 3cfdf9ddf84faedcfc27bd6eb0c2c9286c007395 Mon Sep 17 00:00:00 2001 From: Noam Tzuberi Date: Sun, 3 Jan 2021 18:49:53 +0200 Subject: [PATCH 01/15] add Rise adapter --- modules/riseBidAdapter.js | 250 ++++++++++++++++ modules/riseBidAdapter.md | 51 ++++ test/spec/modules/riseBidAdapter_spec.js | 363 +++++++++++++++++++++++ 3 files changed, 664 insertions(+) create mode 100644 modules/riseBidAdapter.js create mode 100644 modules/riseBidAdapter.md create mode 100644 test/spec/modules/riseBidAdapter_spec.js diff --git a/modules/riseBidAdapter.js b/modules/riseBidAdapter.js new file mode 100644 index 00000000000..d9764349a88 --- /dev/null +++ b/modules/riseBidAdapter.js @@ -0,0 +1,250 @@ +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import * as utils from '../src/utils.js'; +import {VIDEO} from '../src/mediaTypes.js'; +import {config} from '../src/config.js'; + +const SUPPORTED_AD_TYPES = [VIDEO]; +const BIDDER_CODE = 'rise'; +const BIDDER_VERSION = '4.0.0'; +const TTL = 360; +const SELLER_ENDPOINT = 'https://hb.yellowblue.io/'; +const MODES = { + PRODUCTION: 'hb', + TEST: 'hb-test' +} +const SUPPORTED_SYNC_METHODS = { + IFRAME: 'iframe', + PIXEL: 'pixel' +} + +export const spec = { + code: BIDDER_CODE, + version: BIDDER_VERSION, + supportedMediaTypes: SUPPORTED_AD_TYPES, + isBidRequestValid: function(bidRequest) { + return !!(bidRequest.params.isOrg); + }, + buildRequests: function (bidRequests, bidderRequest) { + if (bidRequests.length === 0) { + return []; + } + + const requests = []; + + bidRequests.forEach(bid => { + requests.push(buildVideoRequest(bid, bidderRequest)); + }); + + return requests; + }, + interpretResponse: function({body}) { + const bidResponses = []; + + const bidResponse = { + requestId: body.requestId, + cpm: body.cpm, + width: body.width, + height: body.height, + creativeId: body.requestId, + currency: body.currency, + netRevenue: body.netRevenue, + ttl: body.ttl || TTL, + vastXml: body.vastXml, + mediaType: VIDEO + }; + + bidResponses.push(bidResponse); + + return bidResponses; + }, + getUserSyncs: function(syncOptions, serverResponses) { + const syncs = []; + for (const response of serverResponses) { + if (syncOptions.iframeEnabled && response.body.userSyncURL) { + syncs.push({ + type: 'iframe', + url: response.body.userSyncURL + }); + } + if (syncOptions.pixelEnabled && utils.isArray(response.body.userSyncPixels)) { + const pixels = response.body.userSyncPixels.map(pixel => { + return { + type: 'image', + url: pixel + } + }) + syncs.push(...pixels) + } + } + return syncs; + } +}; + +registerBidder(spec); + +/** + * Build the video request + * @param bid {bid} + * @param bidderRequest {bidderRequest} + * @returns {Object} + */ +function buildVideoRequest(bid, bidderRequest) { + const sellerParams = generateParameters(bid, bidderRequest); + const {params} = bid; + return { + method: 'GET', + url: getEndpoint(params.testMode), + data: sellerParams + }; +} + +/** + * Get the the ad size from the bid + * @param bid {bid} + * @returns {Array} + */ +function getSizes(bid) { + if (utils.deepAccess(bid, 'mediaTypes.video.sizes')) { + return bid.mediaTypes.video.sizes[0]; + } else if (Array.isArray(bid.sizes) && bid.sizes.length > 0) { + return bid.sizes[0]; + } + return []; +} + +/** + * Get schain string value + * @param schainObject {Object} + * @returns {string} + */ +function getSupplyChain(schainObject) { + if (utils.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 !utils.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 = utils.isArray(syncRule.bidders) ? syncRule.bidders : [bidderCode]; + return isInclude && utils.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; +} + +/** + * Generate query parameters for the request + * @param bid {bid} + * @param bidderRequest {bidderRequest} + * @returns {Object} + */ +function generateParameters(bid, bidderRequest) { + const timeout = config.getConfig('bidderTimeout'); + const { syncEnabled, filterSettings } = config.getConfig('userSync'); + const [ width, height ] = getSizes(bid); + const { params } = bid; + const { bidderCode } = bidderRequest; + const domain = window.location.hostname; + + const requestParams = { + auction_start: utils.timestamp(), + ad_unit_code: utils.getBidIdParameter('adUnitCode', bid), + tmax: timeout, + width: width, + height: height, + publisher_id: params.isOrg, + floor_price: params.floorPrice, + ua: navigator.userAgent, + bid_id: utils.getBidIdParameter('bidId', bid), + bidder_request_id: utils.getBidIdParameter('bidderRequestId', bid), + transaction_id: utils.getBidIdParameter('transactionId', bid), + session_id: utils.getBidIdParameter('auctionId', bid), + publisher_name: domain, + site_domain: domain, + bidder_version: BIDDER_VERSION + }; + + if (syncEnabled) { + const allowedSyncMethod = getAllowedSyncMethod(filterSettings, bidderCode); + if (allowedSyncMethod) { + requestParams.cs_method = allowedSyncMethod; + } + } + + if (bidderRequest.uspConsent) { + requestParams.us_privacy = bidderRequest.uspConsent; + } + + if (bidderRequest && bidderRequest.gdprConsent && bidderRequest.gdprConsent.gdprApplies) { + requestParams.gdpr = bidderRequest.gdprConsent.gdprApplies; + requestParams.gdpr_consent = bidderRequest.gdprConsent.consentString; + } + + if (params.ifa) { + requestParams.ifa = params.ifa; + } + + if (bid.schain) { + requestParams.schain = getSupplyChain(bid.schain); + } + + if (bidderRequest && bidderRequest.refererInfo) { + requestParams.referrer = utils.deepAccess(bidderRequest, 'refererInfo.referer'); + requestParams.page_url = config.getConfig('pageUrl') || utils.deepAccess(window, 'location.href'); + } + + return requestParams; +} diff --git a/modules/riseBidAdapter.md b/modules/riseBidAdapter.md new file mode 100644 index 00000000000..6cc81f9df10 --- /dev/null +++ b/modules/riseBidAdapter.md @@ -0,0 +1,51 @@ +#Overview + +Module Name: Rise Bidder Adapter + +Module Type: Bidder Adapter + +Maintainer: prebid-digital-brands@ironsrc.com + + +# Description + +Module that connects to Rise's demand sources. + +The Rise adapter requires setup and approval from the Rise. Please reach out to prebid-digital-brands@ironsrc.com to create an Rise account. + +The adapter supports Video(instream). For the integration, Rise returns content as vastXML and requires the publisher to define the cache url in config passed to Prebid for it to be valid in the auction. + +# Bid Parameters +## Video + +| Name | Scope | Type | Description | Example +| ---- | ----- | ---- | ----------- | ------- +| `isOrg` | required | String | Rise publisher Id provided by your Rise representative | "56f91cd4d3e360002000033" +| `floorPrice` | optional | Number | Minimum price in USD. Misuse of this parameter can impact revenue | 2.00 +| `ifa` | optional | String | The ID for advertisers (also referred to as "IDFA") | "XXX-XXX" +| `testMode` | optional | Boolean | This activates the test mode | false + +# Test Parameters +```javascript +var adUnits = [ + { + code: 'dfp-video-div', + sizes: [[640, 480]], + mediaTypes: { + video: { + playerSize: [[640, 480]], + context: 'instream' + } + }, + bids: [{ + bidder: 'rise', + params: { + isOrg: '56f91cd4d3e360002000033', // Required + floorPrice: 2.00, // Optional + ifa: 'XXX-XXX', // Optional + testMode: false // Optional + } + }] + } + ]; +``` diff --git a/test/spec/modules/riseBidAdapter_spec.js b/test/spec/modules/riseBidAdapter_spec.js new file mode 100644 index 00000000000..3fb8ae14df3 --- /dev/null +++ b/test/spec/modules/riseBidAdapter_spec.js @@ -0,0 +1,363 @@ +import { expect } from 'chai'; +import { spec } from 'modules/riseBidAdapter.js'; +import { newBidder } from 'src/adapters/bidderFactory.js'; +import { config } from 'src/config.js'; +import { VIDEO } from '../../../src/mediaTypes.js'; + +const ENDPOINT = 'https://hb.yellowblue.io/hb'; +const TEST_ENDPOINT = 'https://hb.yellowblue.io/hb-test'; +const TTL = 360; + +describe('riseAdapter', 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 bid = { + 'bidder': spec.code, + 'adUnitCode': 'adunit-code', + 'sizes': [['640', '480']], + 'params': { + 'isOrg': 'jdye8weeyirk00000001' + } + }; + + it('should return true when required params are passed', function () { + expect(spec.isBidRequestValid(bid)).to.equal(true); + }); + + it('should return false when required params are not found', function () { + const newBid = Object.assign({}, bid); + delete newBid.params; + newBid.params = { + 'isOrg': null + }; + expect(spec.isBidRequestValid(newBid)).to.equal(false); + }); + }); + + describe('buildRequests', function () { + const bidRequests = [ + { + 'bidder': spec.code, + 'adUnitCode': 'adunit-code', + 'sizes': [[640, 480]], + 'params': { + 'isOrg': 'jdye8weeyirk00000001' + }, + 'bidId': '299ffc8cca0b87', + 'bidderRequestId': '1144f487e563f9', + 'auctionId': 'bfc420c3-8577-4568-9766-a8a935fb620d', + } + ]; + + const testModeBidRequests = [ + { + 'bidder': spec.code, + 'adUnitCode': 'adunit-code', + 'sizes': [[640, 480]], + 'params': { + 'isOrg': 'jdye8weeyirk00000001', + 'testMode': true + }, + 'bidId': '299ffc8cca0b87', + 'bidderRequestId': '1144f487e563f9', + 'auctionId': 'bfc420c3-8577-4568-9766-a8a935fb620d', + } + ]; + + const bidderRequest = { + bidderCode: 'rise', + } + + it('sends bid request to ENDPOINT via GET', function () { + const requests = spec.buildRequests(bidRequests, bidderRequest); + for (const request of requests) { + expect(request.url).to.equal(ENDPOINT); + expect(request.method).to.equal('GET'); + } + }); + + it('sends bid request to test ENDPOINT via GET', function () { + const requests = spec.buildRequests(testModeBidRequests, bidderRequest); + for (const request of requests) { + expect(request.url).to.equal(TEST_ENDPOINT); + expect(request.method).to.equal('GET'); + } + }); + + it('should send the correct bid Id', function () { + const requests = spec.buildRequests(bidRequests, bidderRequest); + for (const request of requests) { + expect(request.data.bid_id).to.equal('299ffc8cca0b87'); + } + }); + + it('should send the correct width and height', function () { + const requests = spec.buildRequests(bidRequests, bidderRequest); + for (const request of requests) { + expect(request.data).to.be.an('object'); + expect(request.data).to.have.property('width', 640); + expect(request.data).to.have.property('height', 480); + } + }); + + it('should respect syncEnabled option', function() { + config.setConfig({ + userSync: { + syncEnabled: false, + filterSettings: { + all: { + bidders: '*', + filter: 'include' + } + } + } + }); + const requests = spec.buildRequests(bidRequests, bidderRequest); + for (const request of requests) { + expect(request.data).to.be.an('object'); + expect(request.data).to.not.have.property('cs_method'); + } + }); + + it('should respect "iframe" filter settings', function () { + config.setConfig({ + userSync: { + syncEnabled: true, + filterSettings: { + iframe: { + bidders: [spec.code], + filter: 'include' + } + } + } + }); + const requests = spec.buildRequests(bidRequests, bidderRequest); + for (const request of requests) { + expect(request.data).to.be.an('object'); + expect(request.data).to.have.property('cs_method', 'iframe'); + } + }); + + it('should respect "all" filter settings', function () { + config.setConfig({ + userSync: { + syncEnabled: true, + filterSettings: { + all: { + bidders: [spec.code], + filter: 'include' + } + } + } + }); + const requests = spec.buildRequests(bidRequests, bidderRequest); + for (const request of requests) { + expect(request.data).to.be.an('object'); + expect(request.data).to.have.property('cs_method', 'iframe'); + } + }); + + it('should send the pixel user sync param if userSync is enabled and no "iframe" or "all" configs are present', function () { + config.setConfig({ + userSync: { + syncEnabled: true + } + }); + const requests = spec.buildRequests(bidRequests, bidderRequest); + for (const request of requests) { + expect(request.data).to.be.an('object'); + expect(request.data).to.have.property('cs_method', 'pixel'); + } + }); + + it('should respect total exclusion', function() { + config.setConfig({ + userSync: { + syncEnabled: true, + filterSettings: { + image: { + bidders: [spec.code], + filter: 'exclude' + }, + iframe: { + bidders: [spec.code], + filter: 'exclude' + } + } + } + }); + const requests = spec.buildRequests(bidRequests, bidderRequest); + for (const request of requests) { + expect(request.data).to.be.an('object'); + expect(request.data).to.not.have.property('cs_method'); + } + }); + + it('should have us_privacy param if usPrivacy is available in the bidRequest', function () { + const bidderRequestWithUSP = Object.assign({uspConsent: '1YNN'}, bidderRequest); + const requests = spec.buildRequests(bidRequests, bidderRequestWithUSP); + for (const request of requests) { + expect(request.data).to.be.an('object'); + expect(request.data).to.have.property('us_privacy', '1YNN'); + } + }); + + it('should have an empty us_privacy param if usPrivacy is missing in the bidRequest', function () { + const requests = spec.buildRequests(bidRequests, bidderRequest); + for (const request of requests) { + expect(request.data).to.be.an('object'); + expect(request.data).to.not.have.property('us_privacy'); + } + }); + + it('should not send the gdpr param if gdprApplies is false in the bidRequest', function () { + const bidderRequestWithGDPR = Object.assign({gdprConsent: {gdprApplies: false}}, bidderRequest); + const requests = spec.buildRequests(bidRequests, bidderRequestWithGDPR); + for (const request of requests) { + expect(request.data).to.be.an('object'); + expect(request.data).to.not.have.property('gdpr'); + expect(request.data).to.not.have.property('gdpr_consent'); + } + }); + + it('should send the gdpr param if gdprApplies is true in the bidRequest', function () { + const bidderRequestWithGDPR = Object.assign({gdprConsent: {gdprApplies: true, consentString: 'test-consent-string'}}, bidderRequest); + const requests = spec.buildRequests(bidRequests, bidderRequestWithGDPR); + for (const request of requests) { + expect(request.data).to.be.an('object'); + expect(request.data).to.have.property('gdpr', true); + expect(request.data).to.have.property('gdpr_consent', 'test-consent-string'); + } + }); + + it('should have schain param if it is available in the bidRequest', () => { + const schain = { + ver: '1.0', + complete: 1, + nodes: [{ asi: 'indirectseller.com', sid: '00001', hp: 1 }], + }; + bidRequests[0].schain = schain; + const requests = spec.buildRequests(bidRequests, bidderRequest); + for (const request of requests) { + expect(request.data).to.be.an('object'); + expect(request.data).to.have.property('schain', '1.0,1!indirectseller.com,00001,,,,'); + } + }); + }); + + describe('interpretResponse', function () { + const response = { + cpm: 12.5, + vastXml: '', + width: 640, + height: 480, + requestId: '21e12606d47ba7', + netRevenue: true, + currency: 'USD' + }; + + it('should get correct bid response', function () { + let expectedResponse = [ + { + requestId: '21e12606d47ba7', + cpm: 12.5, + width: 640, + height: 480, + creativeId: '21e12606d47ba7', + currency: 'USD', + netRevenue: true, + ttl: TTL, + vastXml: '', + mediaType: VIDEO + } + ]; + const result = spec.interpretResponse({ body: response }); + expect(Object.keys(result[0])).to.have.members(Object.keys(expectedResponse[0])); + }); + }) + + describe('getUserSyncs', function() { + const imageSyncResponse = { + body: { + userSyncPixels: [ + 'https://image-sync-url.test/1', + 'https://image-sync-url.test/2', + 'https://image-sync-url.test/3' + ] + } + }; + + const iframeSyncResponse = { + body: { + userSyncURL: 'https://iframe-sync-url.test' + } + }; + + it('should register all img urls from the response', function() { + const syncs = spec.getUserSyncs({ pixelEnabled: true }, [imageSyncResponse]); + expect(syncs).to.deep.equal([ + { + type: 'image', + url: 'https://image-sync-url.test/1' + }, + { + type: 'image', + url: 'https://image-sync-url.test/2' + }, + { + type: 'image', + url: 'https://image-sync-url.test/3' + } + ]); + }); + + it('should register the iframe url from the response', function() { + const syncs = spec.getUserSyncs({ iframeEnabled: true }, [iframeSyncResponse]); + expect(syncs).to.deep.equal([ + { + type: 'iframe', + url: 'https://iframe-sync-url.test' + } + ]); + }); + + it('should register both image and iframe urls from the responses', function() { + const syncs = spec.getUserSyncs({ pixelEnabled: true, iframeEnabled: true }, [iframeSyncResponse, imageSyncResponse]); + expect(syncs).to.deep.equal([ + { + type: 'iframe', + url: 'https://iframe-sync-url.test' + }, + { + type: 'image', + url: 'https://image-sync-url.test/1' + }, + { + type: 'image', + url: 'https://image-sync-url.test/2' + }, + { + type: 'image', + url: 'https://image-sync-url.test/3' + } + ]); + }); + + it('should handle an empty response', function() { + const syncs = spec.getUserSyncs({ iframeEnabled: true }, []); + expect(syncs).to.deep.equal([]); + }); + + it('should handle when user syncs are disabled', function() { + const syncs = spec.getUserSyncs({ pixelEnabled: false }, [imageSyncResponse]); + expect(syncs).to.deep.equal([]); + }); + }) +}); From 923e646411dada6c1ed53cf74a614b4e1c40ab88 Mon Sep 17 00:00:00 2001 From: Noam Tzuberi Date: Sun, 3 Jan 2021 18:53:09 +0200 Subject: [PATCH 02/15] fixes --- modules/riseBidAdapter.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/riseBidAdapter.md b/modules/riseBidAdapter.md index 6cc81f9df10..0e138b7b977 100644 --- a/modules/riseBidAdapter.md +++ b/modules/riseBidAdapter.md @@ -20,7 +20,7 @@ The adapter supports Video(instream). For the integration, Rise returns content | Name | Scope | Type | Description | Example | ---- | ----- | ---- | ----------- | ------- -| `isOrg` | required | String | Rise publisher Id provided by your Rise representative | "56f91cd4d3e360002000033" +| `isOrg` | required | String | Rise publisher Id provided by your Rise representative | "56f91cd4d3e3660002000033" | `floorPrice` | optional | Number | Minimum price in USD. Misuse of this parameter can impact revenue | 2.00 | `ifa` | optional | String | The ID for advertisers (also referred to as "IDFA") | "XXX-XXX" | `testMode` | optional | Boolean | This activates the test mode | false @@ -40,7 +40,7 @@ var adUnits = [ bids: [{ bidder: 'rise', params: { - isOrg: '56f91cd4d3e360002000033', // Required + isOrg: '56f91cd4d3e3660002000033', // Required floorPrice: 2.00, // Optional ifa: 'XXX-XXX', // Optional testMode: false // Optional From 499f36f178dcc2036dd3b5bb2d146832f8e2519a Mon Sep 17 00:00:00 2001 From: Noam Tzuberi Date: Tue, 5 Jan 2021 15:35:54 +0200 Subject: [PATCH 03/15] change param isOrg to org --- modules/riseBidAdapter.js | 4 ++-- modules/riseBidAdapter.md | 4 ++-- test/spec/modules/riseBidAdapter_spec.js | 8 ++++---- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/modules/riseBidAdapter.js b/modules/riseBidAdapter.js index d9764349a88..8b662fe6232 100644 --- a/modules/riseBidAdapter.js +++ b/modules/riseBidAdapter.js @@ -22,7 +22,7 @@ export const spec = { version: BIDDER_VERSION, supportedMediaTypes: SUPPORTED_AD_TYPES, isBidRequestValid: function(bidRequest) { - return !!(bidRequest.params.isOrg); + return !!(bidRequest.params.org); }, buildRequests: function (bidRequests, bidderRequest) { if (bidRequests.length === 0) { @@ -205,7 +205,7 @@ function generateParameters(bid, bidderRequest) { tmax: timeout, width: width, height: height, - publisher_id: params.isOrg, + publisher_id: params.org, floor_price: params.floorPrice, ua: navigator.userAgent, bid_id: utils.getBidIdParameter('bidId', bid), diff --git a/modules/riseBidAdapter.md b/modules/riseBidAdapter.md index 0e138b7b977..7798e5edb27 100644 --- a/modules/riseBidAdapter.md +++ b/modules/riseBidAdapter.md @@ -20,7 +20,7 @@ The adapter supports Video(instream). For the integration, Rise returns content | Name | Scope | Type | Description | Example | ---- | ----- | ---- | ----------- | ------- -| `isOrg` | required | String | Rise publisher Id provided by your Rise representative | "56f91cd4d3e3660002000033" +| `org` | required | String | Rise publisher Id provided by your Rise representative | "56f91cd4d3e3660002000033" | `floorPrice` | optional | Number | Minimum price in USD. Misuse of this parameter can impact revenue | 2.00 | `ifa` | optional | String | The ID for advertisers (also referred to as "IDFA") | "XXX-XXX" | `testMode` | optional | Boolean | This activates the test mode | false @@ -40,7 +40,7 @@ var adUnits = [ bids: [{ bidder: 'rise', params: { - isOrg: '56f91cd4d3e3660002000033', // Required + org: '56f91cd4d3e3660002000033', // Required floorPrice: 2.00, // Optional ifa: 'XXX-XXX', // Optional testMode: false // Optional diff --git a/test/spec/modules/riseBidAdapter_spec.js b/test/spec/modules/riseBidAdapter_spec.js index 3fb8ae14df3..176437c4f27 100644 --- a/test/spec/modules/riseBidAdapter_spec.js +++ b/test/spec/modules/riseBidAdapter_spec.js @@ -23,7 +23,7 @@ describe('riseAdapter', function () { 'adUnitCode': 'adunit-code', 'sizes': [['640', '480']], 'params': { - 'isOrg': 'jdye8weeyirk00000001' + 'org': 'jdye8weeyirk00000001' } }; @@ -35,7 +35,7 @@ describe('riseAdapter', function () { const newBid = Object.assign({}, bid); delete newBid.params; newBid.params = { - 'isOrg': null + 'org': null }; expect(spec.isBidRequestValid(newBid)).to.equal(false); }); @@ -48,7 +48,7 @@ describe('riseAdapter', function () { 'adUnitCode': 'adunit-code', 'sizes': [[640, 480]], 'params': { - 'isOrg': 'jdye8weeyirk00000001' + 'org': 'jdye8weeyirk00000001' }, 'bidId': '299ffc8cca0b87', 'bidderRequestId': '1144f487e563f9', @@ -62,7 +62,7 @@ describe('riseAdapter', function () { 'adUnitCode': 'adunit-code', 'sizes': [[640, 480]], 'params': { - 'isOrg': 'jdye8weeyirk00000001', + 'org': 'jdye8weeyirk00000001', 'testMode': true }, 'bidId': '299ffc8cca0b87', From d33a8aff7431413e314f3b85544ec92cb461664e Mon Sep 17 00:00:00 2001 From: noamtzuberi Date: Tue, 5 Jan 2021 15:56:32 +0200 Subject: [PATCH 04/15] Rise adapter --- modules/riseBidAdapter.js | 250 ++++++++++++++++ modules/riseBidAdapter.md | 51 ++++ test/spec/modules/riseBidAdapter_spec.js | 363 +++++++++++++++++++++++ 3 files changed, 664 insertions(+) create mode 100644 modules/riseBidAdapter.js create mode 100644 modules/riseBidAdapter.md create mode 100644 test/spec/modules/riseBidAdapter_spec.js diff --git a/modules/riseBidAdapter.js b/modules/riseBidAdapter.js new file mode 100644 index 00000000000..8b662fe6232 --- /dev/null +++ b/modules/riseBidAdapter.js @@ -0,0 +1,250 @@ +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import * as utils from '../src/utils.js'; +import {VIDEO} from '../src/mediaTypes.js'; +import {config} from '../src/config.js'; + +const SUPPORTED_AD_TYPES = [VIDEO]; +const BIDDER_CODE = 'rise'; +const BIDDER_VERSION = '4.0.0'; +const TTL = 360; +const SELLER_ENDPOINT = 'https://hb.yellowblue.io/'; +const MODES = { + PRODUCTION: 'hb', + TEST: 'hb-test' +} +const SUPPORTED_SYNC_METHODS = { + IFRAME: 'iframe', + PIXEL: 'pixel' +} + +export const spec = { + code: BIDDER_CODE, + version: BIDDER_VERSION, + supportedMediaTypes: SUPPORTED_AD_TYPES, + isBidRequestValid: function(bidRequest) { + return !!(bidRequest.params.org); + }, + buildRequests: function (bidRequests, bidderRequest) { + if (bidRequests.length === 0) { + return []; + } + + const requests = []; + + bidRequests.forEach(bid => { + requests.push(buildVideoRequest(bid, bidderRequest)); + }); + + return requests; + }, + interpretResponse: function({body}) { + const bidResponses = []; + + const bidResponse = { + requestId: body.requestId, + cpm: body.cpm, + width: body.width, + height: body.height, + creativeId: body.requestId, + currency: body.currency, + netRevenue: body.netRevenue, + ttl: body.ttl || TTL, + vastXml: body.vastXml, + mediaType: VIDEO + }; + + bidResponses.push(bidResponse); + + return bidResponses; + }, + getUserSyncs: function(syncOptions, serverResponses) { + const syncs = []; + for (const response of serverResponses) { + if (syncOptions.iframeEnabled && response.body.userSyncURL) { + syncs.push({ + type: 'iframe', + url: response.body.userSyncURL + }); + } + if (syncOptions.pixelEnabled && utils.isArray(response.body.userSyncPixels)) { + const pixels = response.body.userSyncPixels.map(pixel => { + return { + type: 'image', + url: pixel + } + }) + syncs.push(...pixels) + } + } + return syncs; + } +}; + +registerBidder(spec); + +/** + * Build the video request + * @param bid {bid} + * @param bidderRequest {bidderRequest} + * @returns {Object} + */ +function buildVideoRequest(bid, bidderRequest) { + const sellerParams = generateParameters(bid, bidderRequest); + const {params} = bid; + return { + method: 'GET', + url: getEndpoint(params.testMode), + data: sellerParams + }; +} + +/** + * Get the the ad size from the bid + * @param bid {bid} + * @returns {Array} + */ +function getSizes(bid) { + if (utils.deepAccess(bid, 'mediaTypes.video.sizes')) { + return bid.mediaTypes.video.sizes[0]; + } else if (Array.isArray(bid.sizes) && bid.sizes.length > 0) { + return bid.sizes[0]; + } + return []; +} + +/** + * Get schain string value + * @param schainObject {Object} + * @returns {string} + */ +function getSupplyChain(schainObject) { + if (utils.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 !utils.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 = utils.isArray(syncRule.bidders) ? syncRule.bidders : [bidderCode]; + return isInclude && utils.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; +} + +/** + * Generate query parameters for the request + * @param bid {bid} + * @param bidderRequest {bidderRequest} + * @returns {Object} + */ +function generateParameters(bid, bidderRequest) { + const timeout = config.getConfig('bidderTimeout'); + const { syncEnabled, filterSettings } = config.getConfig('userSync'); + const [ width, height ] = getSizes(bid); + const { params } = bid; + const { bidderCode } = bidderRequest; + const domain = window.location.hostname; + + const requestParams = { + auction_start: utils.timestamp(), + ad_unit_code: utils.getBidIdParameter('adUnitCode', bid), + tmax: timeout, + width: width, + height: height, + publisher_id: params.org, + floor_price: params.floorPrice, + ua: navigator.userAgent, + bid_id: utils.getBidIdParameter('bidId', bid), + bidder_request_id: utils.getBidIdParameter('bidderRequestId', bid), + transaction_id: utils.getBidIdParameter('transactionId', bid), + session_id: utils.getBidIdParameter('auctionId', bid), + publisher_name: domain, + site_domain: domain, + bidder_version: BIDDER_VERSION + }; + + if (syncEnabled) { + const allowedSyncMethod = getAllowedSyncMethod(filterSettings, bidderCode); + if (allowedSyncMethod) { + requestParams.cs_method = allowedSyncMethod; + } + } + + if (bidderRequest.uspConsent) { + requestParams.us_privacy = bidderRequest.uspConsent; + } + + if (bidderRequest && bidderRequest.gdprConsent && bidderRequest.gdprConsent.gdprApplies) { + requestParams.gdpr = bidderRequest.gdprConsent.gdprApplies; + requestParams.gdpr_consent = bidderRequest.gdprConsent.consentString; + } + + if (params.ifa) { + requestParams.ifa = params.ifa; + } + + if (bid.schain) { + requestParams.schain = getSupplyChain(bid.schain); + } + + if (bidderRequest && bidderRequest.refererInfo) { + requestParams.referrer = utils.deepAccess(bidderRequest, 'refererInfo.referer'); + requestParams.page_url = config.getConfig('pageUrl') || utils.deepAccess(window, 'location.href'); + } + + return requestParams; +} diff --git a/modules/riseBidAdapter.md b/modules/riseBidAdapter.md new file mode 100644 index 00000000000..7798e5edb27 --- /dev/null +++ b/modules/riseBidAdapter.md @@ -0,0 +1,51 @@ +#Overview + +Module Name: Rise Bidder Adapter + +Module Type: Bidder Adapter + +Maintainer: prebid-digital-brands@ironsrc.com + + +# Description + +Module that connects to Rise's demand sources. + +The Rise adapter requires setup and approval from the Rise. Please reach out to prebid-digital-brands@ironsrc.com to create an Rise account. + +The adapter supports Video(instream). For the integration, Rise returns content as vastXML and requires the publisher to define the cache url in config passed to Prebid for it to be valid in the auction. + +# Bid Parameters +## Video + +| Name | Scope | Type | Description | Example +| ---- | ----- | ---- | ----------- | ------- +| `org` | required | String | Rise publisher Id provided by your Rise representative | "56f91cd4d3e3660002000033" +| `floorPrice` | optional | Number | Minimum price in USD. Misuse of this parameter can impact revenue | 2.00 +| `ifa` | optional | String | The ID for advertisers (also referred to as "IDFA") | "XXX-XXX" +| `testMode` | optional | Boolean | This activates the test mode | false + +# Test Parameters +```javascript +var adUnits = [ + { + code: 'dfp-video-div', + sizes: [[640, 480]], + mediaTypes: { + video: { + playerSize: [[640, 480]], + context: 'instream' + } + }, + bids: [{ + bidder: 'rise', + params: { + org: '56f91cd4d3e3660002000033', // Required + floorPrice: 2.00, // Optional + ifa: 'XXX-XXX', // Optional + testMode: false // Optional + } + }] + } + ]; +``` diff --git a/test/spec/modules/riseBidAdapter_spec.js b/test/spec/modules/riseBidAdapter_spec.js new file mode 100644 index 00000000000..176437c4f27 --- /dev/null +++ b/test/spec/modules/riseBidAdapter_spec.js @@ -0,0 +1,363 @@ +import { expect } from 'chai'; +import { spec } from 'modules/riseBidAdapter.js'; +import { newBidder } from 'src/adapters/bidderFactory.js'; +import { config } from 'src/config.js'; +import { VIDEO } from '../../../src/mediaTypes.js'; + +const ENDPOINT = 'https://hb.yellowblue.io/hb'; +const TEST_ENDPOINT = 'https://hb.yellowblue.io/hb-test'; +const TTL = 360; + +describe('riseAdapter', 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 bid = { + 'bidder': spec.code, + 'adUnitCode': 'adunit-code', + 'sizes': [['640', '480']], + 'params': { + 'org': 'jdye8weeyirk00000001' + } + }; + + it('should return true when required params are passed', function () { + expect(spec.isBidRequestValid(bid)).to.equal(true); + }); + + it('should return false when required params are not found', function () { + const newBid = Object.assign({}, bid); + delete newBid.params; + newBid.params = { + 'org': null + }; + expect(spec.isBidRequestValid(newBid)).to.equal(false); + }); + }); + + describe('buildRequests', function () { + const bidRequests = [ + { + 'bidder': spec.code, + 'adUnitCode': 'adunit-code', + 'sizes': [[640, 480]], + 'params': { + 'org': 'jdye8weeyirk00000001' + }, + 'bidId': '299ffc8cca0b87', + 'bidderRequestId': '1144f487e563f9', + 'auctionId': 'bfc420c3-8577-4568-9766-a8a935fb620d', + } + ]; + + const testModeBidRequests = [ + { + 'bidder': spec.code, + 'adUnitCode': 'adunit-code', + 'sizes': [[640, 480]], + 'params': { + 'org': 'jdye8weeyirk00000001', + 'testMode': true + }, + 'bidId': '299ffc8cca0b87', + 'bidderRequestId': '1144f487e563f9', + 'auctionId': 'bfc420c3-8577-4568-9766-a8a935fb620d', + } + ]; + + const bidderRequest = { + bidderCode: 'rise', + } + + it('sends bid request to ENDPOINT via GET', function () { + const requests = spec.buildRequests(bidRequests, bidderRequest); + for (const request of requests) { + expect(request.url).to.equal(ENDPOINT); + expect(request.method).to.equal('GET'); + } + }); + + it('sends bid request to test ENDPOINT via GET', function () { + const requests = spec.buildRequests(testModeBidRequests, bidderRequest); + for (const request of requests) { + expect(request.url).to.equal(TEST_ENDPOINT); + expect(request.method).to.equal('GET'); + } + }); + + it('should send the correct bid Id', function () { + const requests = spec.buildRequests(bidRequests, bidderRequest); + for (const request of requests) { + expect(request.data.bid_id).to.equal('299ffc8cca0b87'); + } + }); + + it('should send the correct width and height', function () { + const requests = spec.buildRequests(bidRequests, bidderRequest); + for (const request of requests) { + expect(request.data).to.be.an('object'); + expect(request.data).to.have.property('width', 640); + expect(request.data).to.have.property('height', 480); + } + }); + + it('should respect syncEnabled option', function() { + config.setConfig({ + userSync: { + syncEnabled: false, + filterSettings: { + all: { + bidders: '*', + filter: 'include' + } + } + } + }); + const requests = spec.buildRequests(bidRequests, bidderRequest); + for (const request of requests) { + expect(request.data).to.be.an('object'); + expect(request.data).to.not.have.property('cs_method'); + } + }); + + it('should respect "iframe" filter settings', function () { + config.setConfig({ + userSync: { + syncEnabled: true, + filterSettings: { + iframe: { + bidders: [spec.code], + filter: 'include' + } + } + } + }); + const requests = spec.buildRequests(bidRequests, bidderRequest); + for (const request of requests) { + expect(request.data).to.be.an('object'); + expect(request.data).to.have.property('cs_method', 'iframe'); + } + }); + + it('should respect "all" filter settings', function () { + config.setConfig({ + userSync: { + syncEnabled: true, + filterSettings: { + all: { + bidders: [spec.code], + filter: 'include' + } + } + } + }); + const requests = spec.buildRequests(bidRequests, bidderRequest); + for (const request of requests) { + expect(request.data).to.be.an('object'); + expect(request.data).to.have.property('cs_method', 'iframe'); + } + }); + + it('should send the pixel user sync param if userSync is enabled and no "iframe" or "all" configs are present', function () { + config.setConfig({ + userSync: { + syncEnabled: true + } + }); + const requests = spec.buildRequests(bidRequests, bidderRequest); + for (const request of requests) { + expect(request.data).to.be.an('object'); + expect(request.data).to.have.property('cs_method', 'pixel'); + } + }); + + it('should respect total exclusion', function() { + config.setConfig({ + userSync: { + syncEnabled: true, + filterSettings: { + image: { + bidders: [spec.code], + filter: 'exclude' + }, + iframe: { + bidders: [spec.code], + filter: 'exclude' + } + } + } + }); + const requests = spec.buildRequests(bidRequests, bidderRequest); + for (const request of requests) { + expect(request.data).to.be.an('object'); + expect(request.data).to.not.have.property('cs_method'); + } + }); + + it('should have us_privacy param if usPrivacy is available in the bidRequest', function () { + const bidderRequestWithUSP = Object.assign({uspConsent: '1YNN'}, bidderRequest); + const requests = spec.buildRequests(bidRequests, bidderRequestWithUSP); + for (const request of requests) { + expect(request.data).to.be.an('object'); + expect(request.data).to.have.property('us_privacy', '1YNN'); + } + }); + + it('should have an empty us_privacy param if usPrivacy is missing in the bidRequest', function () { + const requests = spec.buildRequests(bidRequests, bidderRequest); + for (const request of requests) { + expect(request.data).to.be.an('object'); + expect(request.data).to.not.have.property('us_privacy'); + } + }); + + it('should not send the gdpr param if gdprApplies is false in the bidRequest', function () { + const bidderRequestWithGDPR = Object.assign({gdprConsent: {gdprApplies: false}}, bidderRequest); + const requests = spec.buildRequests(bidRequests, bidderRequestWithGDPR); + for (const request of requests) { + expect(request.data).to.be.an('object'); + expect(request.data).to.not.have.property('gdpr'); + expect(request.data).to.not.have.property('gdpr_consent'); + } + }); + + it('should send the gdpr param if gdprApplies is true in the bidRequest', function () { + const bidderRequestWithGDPR = Object.assign({gdprConsent: {gdprApplies: true, consentString: 'test-consent-string'}}, bidderRequest); + const requests = spec.buildRequests(bidRequests, bidderRequestWithGDPR); + for (const request of requests) { + expect(request.data).to.be.an('object'); + expect(request.data).to.have.property('gdpr', true); + expect(request.data).to.have.property('gdpr_consent', 'test-consent-string'); + } + }); + + it('should have schain param if it is available in the bidRequest', () => { + const schain = { + ver: '1.0', + complete: 1, + nodes: [{ asi: 'indirectseller.com', sid: '00001', hp: 1 }], + }; + bidRequests[0].schain = schain; + const requests = spec.buildRequests(bidRequests, bidderRequest); + for (const request of requests) { + expect(request.data).to.be.an('object'); + expect(request.data).to.have.property('schain', '1.0,1!indirectseller.com,00001,,,,'); + } + }); + }); + + describe('interpretResponse', function () { + const response = { + cpm: 12.5, + vastXml: '', + width: 640, + height: 480, + requestId: '21e12606d47ba7', + netRevenue: true, + currency: 'USD' + }; + + it('should get correct bid response', function () { + let expectedResponse = [ + { + requestId: '21e12606d47ba7', + cpm: 12.5, + width: 640, + height: 480, + creativeId: '21e12606d47ba7', + currency: 'USD', + netRevenue: true, + ttl: TTL, + vastXml: '', + mediaType: VIDEO + } + ]; + const result = spec.interpretResponse({ body: response }); + expect(Object.keys(result[0])).to.have.members(Object.keys(expectedResponse[0])); + }); + }) + + describe('getUserSyncs', function() { + const imageSyncResponse = { + body: { + userSyncPixels: [ + 'https://image-sync-url.test/1', + 'https://image-sync-url.test/2', + 'https://image-sync-url.test/3' + ] + } + }; + + const iframeSyncResponse = { + body: { + userSyncURL: 'https://iframe-sync-url.test' + } + }; + + it('should register all img urls from the response', function() { + const syncs = spec.getUserSyncs({ pixelEnabled: true }, [imageSyncResponse]); + expect(syncs).to.deep.equal([ + { + type: 'image', + url: 'https://image-sync-url.test/1' + }, + { + type: 'image', + url: 'https://image-sync-url.test/2' + }, + { + type: 'image', + url: 'https://image-sync-url.test/3' + } + ]); + }); + + it('should register the iframe url from the response', function() { + const syncs = spec.getUserSyncs({ iframeEnabled: true }, [iframeSyncResponse]); + expect(syncs).to.deep.equal([ + { + type: 'iframe', + url: 'https://iframe-sync-url.test' + } + ]); + }); + + it('should register both image and iframe urls from the responses', function() { + const syncs = spec.getUserSyncs({ pixelEnabled: true, iframeEnabled: true }, [iframeSyncResponse, imageSyncResponse]); + expect(syncs).to.deep.equal([ + { + type: 'iframe', + url: 'https://iframe-sync-url.test' + }, + { + type: 'image', + url: 'https://image-sync-url.test/1' + }, + { + type: 'image', + url: 'https://image-sync-url.test/2' + }, + { + type: 'image', + url: 'https://image-sync-url.test/3' + } + ]); + }); + + it('should handle an empty response', function() { + const syncs = spec.getUserSyncs({ iframeEnabled: true }, []); + expect(syncs).to.deep.equal([]); + }); + + it('should handle when user syncs are disabled', function() { + const syncs = spec.getUserSyncs({ pixelEnabled: false }, [imageSyncResponse]); + expect(syncs).to.deep.equal([]); + }); + }) +}); From b2cdd97fee0b47e7346ffc8204ac2857d30512e1 Mon Sep 17 00:00:00 2001 From: Noam Tzuberi Date: Tue, 5 Jan 2021 16:56:42 +0200 Subject: [PATCH 05/15] change email for rise --- modules/riseBidAdapter.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/riseBidAdapter.md b/modules/riseBidAdapter.md index 7798e5edb27..67eeab18226 100644 --- a/modules/riseBidAdapter.md +++ b/modules/riseBidAdapter.md @@ -4,14 +4,14 @@ Module Name: Rise Bidder Adapter Module Type: Bidder Adapter -Maintainer: prebid-digital-brands@ironsrc.com +Maintainer: prebid-rise-engage@risecodes.com # Description Module that connects to Rise's demand sources. -The Rise adapter requires setup and approval from the Rise. Please reach out to prebid-digital-brands@ironsrc.com to create an Rise account. +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). For the integration, Rise returns content as vastXML and requires the publisher to define the cache url in config passed to Prebid for it to be valid in the auction. From 3ce6a3bec9b95d3a1acc660995469e152562f567 Mon Sep 17 00:00:00 2001 From: Noam Tzuberi Date: Tue, 5 Jan 2021 18:24:53 +0200 Subject: [PATCH 06/15] fix circle failed --- modules/riseBidAdapter.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/riseBidAdapter.js b/modules/riseBidAdapter.js index 8b662fe6232..a891e0aa883 100644 --- a/modules/riseBidAdapter.js +++ b/modules/riseBidAdapter.js @@ -193,7 +193,7 @@ function getEndpoint(testMode) { */ function generateParameters(bid, bidderRequest) { const timeout = config.getConfig('bidderTimeout'); - const { syncEnabled, filterSettings } = config.getConfig('userSync'); + const { syncEnabled, filterSettings } = config.getConfig('userSync') || {}; const [ width, height ] = getSizes(bid); const { params } = bid; const { bidderCode } = bidderRequest; From dd10c2557151ef915310257397fef10612b6fb20 Mon Sep 17 00:00:00 2001 From: Noam Tzuberi Date: Tue, 5 Jan 2021 18:39:20 +0200 Subject: [PATCH 07/15] bump From 464d2b742ba59bc8366536cb8a735ec1cc2af036 Mon Sep 17 00:00:00 2001 From: Noam Tzuberi Date: Tue, 5 Jan 2021 18:40:20 +0200 Subject: [PATCH 08/15] bump From 1c520667183a4ac04550fead944b56875f9fe097 Mon Sep 17 00:00:00 2001 From: Noam Tzuberi Date: Tue, 5 Jan 2021 18:41:31 +0200 Subject: [PATCH 09/15] bump --- modules/riseBidAdapter.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/riseBidAdapter.js b/modules/riseBidAdapter.js index a891e0aa883..209ff06240f 100644 --- a/modules/riseBidAdapter.js +++ b/modules/riseBidAdapter.js @@ -191,7 +191,7 @@ function getEndpoint(testMode) { * @param bidderRequest {bidderRequest} * @returns {Object} */ -function generateParameters(bid, bidderRequest) { +function generateParameters(bid, bidderRequest) { const timeout = config.getConfig('bidderTimeout'); const { syncEnabled, filterSettings } = config.getConfig('userSync') || {}; const [ width, height ] = getSizes(bid); From 2b1fad5c1ae1ba38e0a7c47d48fd317fcfad90bf Mon Sep 17 00:00:00 2001 From: Noam Tzuberi Date: Wed, 6 Jan 2021 08:38:50 +0200 Subject: [PATCH 10/15] remove space --- modules/riseBidAdapter.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/riseBidAdapter.js b/modules/riseBidAdapter.js index 209ff06240f..a891e0aa883 100644 --- a/modules/riseBidAdapter.js +++ b/modules/riseBidAdapter.js @@ -191,7 +191,7 @@ function getEndpoint(testMode) { * @param bidderRequest {bidderRequest} * @returns {Object} */ -function generateParameters(bid, bidderRequest) { +function generateParameters(bid, bidderRequest) { const timeout = config.getConfig('bidderTimeout'); const { syncEnabled, filterSettings } = config.getConfig('userSync') || {}; const [ width, height ] = getSizes(bid); From 924bb156405742e3d104913dd25cadf9e0417713 Mon Sep 17 00:00:00 2001 From: Noam Tzuberi Date: Tue, 6 Jul 2021 15:13:44 +0300 Subject: [PATCH 11/15] Upgrade Rise adapter to 5.0 --- modules/riseBidAdapter.js | 26 +++++++++++++++-- test/spec/modules/riseBidAdapter_spec.js | 37 ++++++++++++++++++++++-- 2 files changed, 59 insertions(+), 4 deletions(-) diff --git a/modules/riseBidAdapter.js b/modules/riseBidAdapter.js index a891e0aa883..b03c5c15056 100644 --- a/modules/riseBidAdapter.js +++ b/modules/riseBidAdapter.js @@ -5,8 +5,9 @@ import {config} from '../src/config.js'; const SUPPORTED_AD_TYPES = [VIDEO]; const BIDDER_CODE = 'rise'; -const BIDDER_VERSION = '4.0.0'; +const BIDDER_VERSION = '4.0.1'; const TTL = 360; +const CURRENCY = 'USD'; const SELLER_ENDPOINT = 'https://hb.yellowblue.io/'; const MODES = { PRODUCTION: 'hb', @@ -53,6 +54,10 @@ export const spec = { mediaType: VIDEO }; + if (body.adomain && body.adomain.length) { + bidResponse.meta = {}; + bidResponse.meta.advertiserDomains = body.adomain + } bidResponses.push(bidResponse); return bidResponses; @@ -82,6 +87,23 @@ export const spec = { registerBidder(spec); +/** + * Get floor price + * @param bid {bid} + * @returns {Number} + */ +function getFloor(bid) { + if (!utils.isFn(bid.getFloor)) { + return 0; + } + let floorResult = bid.getFloor({ + currency: CURRENCY, + mediaType: VIDEO, + size: '*' + }); + return floorResult.currency === CURRENCY ? floorResult.floor : 0; +} + /** * Build the video request * @param bid {bid} @@ -206,7 +228,7 @@ function generateParameters(bid, bidderRequest) { width: width, height: height, publisher_id: params.org, - floor_price: params.floorPrice, + floor_price: Math.max(getFloor(bid), params.floorPrice), ua: navigator.userAgent, bid_id: utils.getBidIdParameter('bidId', bid), bidder_request_id: utils.getBidIdParameter('bidderRequestId', bid), diff --git a/test/spec/modules/riseBidAdapter_spec.js b/test/spec/modules/riseBidAdapter_spec.js index 176437c4f27..b6c2f4fc61a 100644 --- a/test/spec/modules/riseBidAdapter_spec.js +++ b/test/spec/modules/riseBidAdapter_spec.js @@ -3,6 +3,7 @@ import { spec } from 'modules/riseBidAdapter.js'; import { newBidder } from 'src/adapters/bidderFactory.js'; import { config } from 'src/config.js'; import { VIDEO } from '../../../src/mediaTypes.js'; +import { deepClone } from 'src/utils.js'; const ENDPOINT = 'https://hb.yellowblue.io/hb'; const TEST_ENDPOINT = 'https://hb.yellowblue.io/hb-test'; @@ -250,6 +251,34 @@ describe('riseAdapter', function () { expect(request.data).to.have.property('schain', '1.0,1!indirectseller.com,00001,,,,'); } }); + + it('should set floor_price to getFloor.floor value if it is greater than params.floorPrice', function() { + const bid = deepClone(bidRequests[0]); + bid.getFloor = () => { + return { + currency: 'USD', + floor: 3.32 + } + } + bid.params.floorPrice = 0.64; + const request = spec.buildRequests([bid], bidderRequest)[0]; + expect(request.data).to.be.an('object'); + expect(request.data).to.have.property('floor_price', 3.32); + }); + + it('should set floor_price to params.floorPrice value if it is greater than getFloor.floor', function() { + const bid = deepClone(bidRequests[0]); + bid.getFloor = () => { + return { + currency: 'USD', + floor: 0.8 + } + } + bid.params.floorPrice = 1.5; + const request = spec.buildRequests([bid], bidderRequest)[0]; + expect(request.data).to.be.an('object'); + expect(request.data).to.have.property('floor_price', 1.5); + }); }); describe('interpretResponse', function () { @@ -260,7 +289,8 @@ describe('riseAdapter', function () { height: 480, requestId: '21e12606d47ba7', netRevenue: true, - currency: 'USD' + currency: 'USD', + adomain: ['abc.com'] }; it('should get correct bid response', function () { @@ -275,7 +305,10 @@ describe('riseAdapter', function () { netRevenue: true, ttl: TTL, vastXml: '', - mediaType: VIDEO + mediaType: VIDEO, + meta: { + advertiserDomains: ['abc.com'] + } } ]; const result = spec.interpretResponse({ body: response }); From 5c237aae99d3311e3662c017e3ae1e17b8ac9af4 Mon Sep 17 00:00:00 2001 From: Laslo Chechur Date: Wed, 8 Jun 2022 15:52:19 +0300 Subject: [PATCH 12/15] add kueezBidAdapter --- modules/kueezBidAdapter.js | 470 +++++++++++++++++++++ modules/kueezBidAdapter.md | 49 +++ test/spec/modules/kueezBidAdapter_spec.js | 479 ++++++++++++++++++++++ 3 files changed, 998 insertions(+) create mode 100644 modules/kueezBidAdapter.js create mode 100644 modules/kueezBidAdapter.md create mode 100644 test/spec/modules/kueezBidAdapter_spec.js diff --git a/modules/kueezBidAdapter.js b/modules/kueezBidAdapter.js new file mode 100644 index 00000000000..73265c8df4f --- /dev/null +++ b/modules/kueezBidAdapter.js @@ -0,0 +1,470 @@ +import { logWarn, logInfo, isArray, isFn, deepAccess, isEmpty, contains, timestamp, getBidIdParameter, triggerPixel, isInteger } 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 BIDDER_ENDPOINT = 'https://hb.kueezssp.com/hb-kz-multi'; +const BIDDER_TEST_ENDPOINT = 'https://hb.kueezssp.com/hb-multi-kz-test' +const BIDDER_CODE = 'kueez'; +const MAIN_CURRENCY = 'USD'; +const MEDIA_TYPES = [BANNER, VIDEO]; +const TTL = 420; +const VERSION = '1.0.0'; +const SUPPORTED_SYNC_METHODS = { + IFRAME: 'iframe', + PIXEL: 'pixel' +} + +export const spec = { + code: BIDDER_CODE, + version: VERSION, + supportedMediaTypes: MEDIA_TYPES, + isBidRequestValid: function (bidRequest) { + return validateParams(bidRequest); + }, + buildRequests: function (validBidRequests, bidderRequest) { + const [ sharedParams ] = validBidRequests; + const testMode = sharedParams.params.testMode; + const bidsToSend = prepareBids(validBidRequests, sharedParams, bidderRequest); + + return { + method: 'POST', + url: getBidderEndpoint(testMode), + data: bidsToSend + } + }, + interpretResponse: function ({body}) { + const bidResponses = body?.bids; + + if (!bidResponses || !bidResponses.length) { + return []; + } + + return parseBidResponses(bidResponses); + }, + getUserSyncs: function (syncOptions, serverResponses) { + const syncs = []; + for (const response of serverResponses) { + if (syncOptions.pixelEnabled && isArray(response.body.params.userSyncPixels)) { + const pixels = response.body.params.userSyncPixels.map(pixel => { + return { + type: 'image', + url: pixel + } + }) + syncs.push(...pixels) + } + if (syncOptions.iframeEnabled && response.body.params.userSyncURL) { + syncs.push({ + type: 'iframe', + url: response.body.params.userSyncURL + }); + } + } + 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 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 the encoded value + * @param val {string} + * @returns {string} + */ +function getEncodedValIfNotEmpty(val) { + return !isEmpty(val) ? encodeURIComponent(val) : ''; +} + +/** + * get device type + * @returns {string} + */ +function getDeviceType() { + const ua = navigator.userAgent; + 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'; +} + +/** + * Get floor price + * @param bid {bid} + * @param mediaType {string} + * @returns {Number} + */ +function getFloorPrice(bid, mediaType) { + let floor = 0; + + if (isFn(bid.getFloor)) { + let floorResult = bid.getFloor({ + currency: MAIN_CURRENCY, + mediaType: mediaType, + size: '*' + }); + floor = floorResult.currency === MAIN_CURRENCY && floorResult.floor ? floorResult.floor : 0; + } + + return floor; +} + +/** + * Get the ad sizes array from the bid + * @param bid {bid} + * @param mediaType {string} + * @returns {Array} + */ +function getSizesArray(bid, mediaType) { + let sizes = [] + + if (deepAccess(bid, `mediaTypes.${mediaType}.sizes`)) { + sizes = bid.mediaTypes[mediaType].sizes; + } else if (Array.isArray(bid.sizes) && bid.sizes.length > 0) { + sizes = bid.sizes; + } + + return sizes; +} + +/** + * Get the preferred user-sync method + * @param filterSettings {filterSettings} + * @param bidderCode {string} + * @returns {string} + */ +function getSyncMethod(filterSettings, bidderCode) { + const iframeConfigs = ['all', 'iframe']; + const pixelConfig = 'image'; + if (filterSettings && iframeConfigs.some(config => isSyncMethodAllowed(filterSettings[config], bidderCode))) { + return SUPPORTED_SYNC_METHODS.IFRAME; + } + if (!filterSettings || !filterSettings[pixelConfig] || isSyncMethodAllowed(filterSettings[pixelConfig], bidderCode)) { + return SUPPORTED_SYNC_METHODS.PIXEL; + } +} + +/** + * Check sync rule support + * @param filterSetting {Object} + * @param bidderCode {string} + * @returns {boolean} + */ +function isSyncMethodAllowed(filterSetting, bidderCode) { + if (!filterSetting) { + return false; + } + const bidders = isArray(filterSetting.bidders) ? filterSetting.bidders : [bidderCode]; + return filterSetting.filter === 'include' && contains(bidders, bidderCode); +} + +/** + * Get the bidder endpoint + * @param testMode {boolean} + * @returns {string} + */ +function getBidderEndpoint(testMode) { + return testMode ? BIDDER_TEST_ENDPOINT : BIDDER_ENDPOINT; +} + +/** + * Generates the bidder parameters + * @param validBidRequests {Array} + * @param bidderRequest {bidderRequest} + * @returns {Array} + */ +function generateBidParams(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); + const gpid = deepAccess(bid, `ortb2Imp.ext.gpid`); + const pos = deepAccess(bid, `mediaTypes.${mediaType}.pos`); + const placementId = params.placementId || deepAccess(bid, `mediaTypes.${mediaType}.name`); + const paramsFloorPrice = isNaN(params.floorPrice) ? 0 : params.floorPrice; + + const bidObject = { + adUnitCode: getBidIdParameter('adUnitCode', bid), + bidId: getBidIdParameter('bidId', bid), + bidderRequestId: getBidIdParameter('bidderRequestId', bid), + floorPrice: Math.max(getFloorPrice(bid, mediaType), paramsFloorPrice), + mediaType, + sizes: sizesArray, + transactionId: getBidIdParameter('transactionId', bid) + }; + + if (pos) { + bidObject.pos = pos; + } + + if (gpid) { + bidObject.gpid = gpid; + } + + if (placementId) { + bidObject.placementId = placementId; + } + + if (mediaType === VIDEO) { + populateVideoParams(bidObject, bid); + } + + return bidObject; +} + +/** + * Checks if the media type is a banner + * @param bid {bid} + * @returns {boolean} + */ +function isBanner(bid) { + return bid.mediaTypes && bid.mediaTypes.banner; +} + +/** + * Generate params that are common between all bids + * @param sharedParams {sharedParams} + * @param bidderRequest {bidderRequest} + * @returns {object} the common params object + */ +function generateSharedParams(sharedParams, bidderRequest) { + const {bidderCode} = bidderRequest; + const {syncEnabled, filterSettings} = config.getConfig('userSync') || {}; + const domain = window.location.hostname; + const generalBidParams = getBidIdParameter('params', sharedParams); + const userIds = getBidIdParameter('userId', sharedParams); + const ortb2Metadata = config.getConfig('ortb2') || {}; + const timeout = config.getConfig('bidderTimeout'); + + const params = { + adapter_version: VERSION, + auction_start: timestamp(), + device_type: getDeviceType(), + dnt: (navigator.doNotTrack === 'yes' || navigator.doNotTrack === '1' || navigator.msDoNotTrack === '1') ? 1 : 0, + publisher_id: generalBidParams.publisherId, + publisher_name: domain, + session_id: getBidIdParameter('auctionId', sharedParams), + site_domain: domain, + tmax: timeout, + ua: navigator.userAgent, + wrapper_type: 'prebidjs', + wrapper_vendor: '$$PREBID_GLOBAL$$', + wrapper_version: '$prebid.version$' + } + + if (syncEnabled) { + const allowedSyncMethod = getSyncMethod(filterSettings, bidderCode); + if (allowedSyncMethod) { + params.cs_method = allowedSyncMethod; + } + } + + if (bidderRequest && bidderRequest.gdprConsent && bidderRequest.gdprConsent.gdprApplies) { + params.gdpr = bidderRequest.gdprConsent.gdprApplies; + params.gdpr_consent = bidderRequest.gdprConsent.consentString; + } + + if (bidderRequest.uspConsent) { + params.us_privacy = bidderRequest.uspConsent; + } + + if (generalBidParams.ifa) { + params.ifa = generalBidParams.ifa; + } + + if (ortb2Metadata.site) { + params.site_metadata = JSON.stringify(ortb2Metadata.site); + } + + if (ortb2Metadata.user) { + params.user_metadata = JSON.stringify(ortb2Metadata.user); + } + + if (bidderRequest && bidderRequest.refererInfo) { + params.page_url = config.getConfig('pageUrl') || window.location.href; + params.referrer = bidderRequest.refererInfo.referer; + } + + if (sharedParams.schain) { + params.schain = getSupplyChain(sharedParams.schain); + } + + if (userIds) { + params.userIds = JSON.stringify(userIds); + } + + return params +} + +/** + * Validates the bidder params + * @param bidRequest {bidRequest} + * @returns {boolean} + */ +function validateParams(bidRequest) { + let isValid = true; + + if (!bidRequest.params) { + logWarn('Kueez adapter - missing params'); + isValid = false; + } + + if (!bidRequest.params.publisherId) { + logWarn('Kueez adapter - publisherId is a required param'); + isValid = false; + } + + return isValid; +} + +/** + * Validates the bidder params + * @param validBidRequests {Array} + * @param sharedParams {sharedParams} + * @param bidderRequest {bidderRequest} + * @returns {Object} + */ +function prepareBids(validBidRequests, sharedParams, bidderRequest) { + return { + params: generateSharedParams(sharedParams, bidderRequest), + bids: generateBidParams(validBidRequests, bidderRequest) + } +} + +function getPlaybackMethod(bid) { + const playbackMethod = deepAccess(bid, `mediaTypes.video.playbackmethod`); + + if (Array.isArray(playbackMethod) && isInteger(playbackMethod[0])) { + return playbackMethod[0]; + } else if (isInteger(playbackMethod)) { + return playbackMethod; + } +} + +function populateVideoParams(params, bid) { + const linearity = deepAccess(bid, `mediaTypes.video.linearity`); + const maxDuration = deepAccess(bid, `mediaTypes.video.maxduration`); + const minDuration = deepAccess(bid, `mediaTypes.video.minduration`); + const placement = deepAccess(bid, `mediaTypes.video.placement`); + const playbackMethod = getPlaybackMethod(bid); + const skip = deepAccess(bid, `mediaTypes.video.skip`); + + if (linearity) { + params.linearity = linearity; + } + + if (maxDuration) { + params.maxDuration = maxDuration; + } + + if (minDuration) { + params.minDuration = minDuration; + } + + if (placement) { + params.placement = placement; + } + + if (playbackMethod) { + params.playbackMethod = playbackMethod; + } + + if (skip) { + params.skip = skip; + } +} + +/** + * Processes the bid responses + * @param bids {Array} + * @returns {Array} + */ +function parseBidResponses(bids) { + return bids.map(bid => { + const bidResponse = { + cpm: bid.cpm, + creativeId: bid.requestId, + currency: bid.currency || MAIN_CURRENCY, + height: bid.height, + mediaType: bid.mediaType, + meta: { + mediaType: bid.mediaType + }, + netRevenue: bid.netRevenue || true, + nurl: bid.nurl, + requestId: bid.requestId, + ttl: bid.ttl || TTL, + width: bid.width + }; + + if (bid.adomain && bid.adomain.length) { + bidResponse.meta.advertiserDomains = bid.adomain; + } + + if (bid.mediaType === VIDEO) { + bidResponse.vastXml = bid.vastXml; + } else if (bid.mediaType === BANNER) { + bidResponse.ad = bid.ad; + } + + return bidResponse; + }); +} diff --git a/modules/kueezBidAdapter.md b/modules/kueezBidAdapter.md new file mode 100644 index 00000000000..49c737f6b39 --- /dev/null +++ b/modules/kueezBidAdapter.md @@ -0,0 +1,49 @@ +#Overview + +Module Name: Kueez Bidder Adapter + +Module Type: Bidder Adapter + +Maintainer: prebid@kueez.com + + +# Description + +The Kueez adapter requires setup and approval from the Kueez team. Please reach out to prebid@kueez.com for more information. + +The adapter supports Banner and Video(instream) media types. + +# Bid Parameters +## Video + +| Name | Scope | Type | Description | Example +|---------------| ----- | ---- |-------------------------------------------------------------------| ------- +| `publisherId` | required | String | the publisher Id provided by your Kueez representative | "test-publisher-id" +| `floorPrice` | optional | Number | Minimum price in USD. Misuse of this parameter can impact revenue | 1.50 +| `placementId` | optional | String | A unique placement identifier | "12345678" +| `testMode` | optional | Boolean | This activates the test mode | false + +# Test Parameters +```javascript +var adUnits = [ + { + code: 'dfp-video-div', + sizes: [[640, 480]], + mediaTypes: { + video: { + playerSize: [[640, 480]], + context: 'instream' + } + }, + bids: [{ + bidder: 'kueez', + params: { + org: 'test-publisher-id', // Required + floorPrice: 1.50, // Optional + placementId: '12345678', // Optional + testMode: false // Optional + } + }] + } + ]; +``` diff --git a/test/spec/modules/kueezBidAdapter_spec.js b/test/spec/modules/kueezBidAdapter_spec.js new file mode 100644 index 00000000000..b7c78d48426 --- /dev/null +++ b/test/spec/modules/kueezBidAdapter_spec.js @@ -0,0 +1,479 @@ +import { expect } from 'chai'; +import { spec } from 'modules/kueezBidAdapter.js'; +import { newBidder } from 'src/adapters/bidderFactory.js'; +import { config } from 'src/config.js'; +import { BANNER, VIDEO } from '../../../src/mediaTypes.js'; +import * as utils from 'src/utils.js'; + +const ENDPOINT = 'https://hb.kueezssp.com/hb-kz-multi'; +const TEST_ENDPOINT = 'https://hb.kueezssp.com/hb-multi-kz-test'; +const TTL = 360; +/* eslint no-console: ["error", { allow: ["log", "warn", "error"] }] */ + +describe('kueezBidAdapter', 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 bid = { + 'bidder': spec.code, + 'adUnitCode': 'adunit-code', + 'sizes': [['640', '480']], + 'params': { + 'publisherId': 'test-publisher-id' + } + }; + + it('should return true when required params are passed', function () { + expect(spec.isBidRequestValid(bid)).to.equal(true); + }); + + it('should return false when required params are not found', function () { + const newBid = Object.assign({}, bid); + delete newBid.params; + newBid.params = { + 'publisherId': null + }; + expect(spec.isBidRequestValid(newBid)).to.equal(false); + }); + }); + + describe('buildRequests', function () { + const bidRequests = [ + { + 'bidder': spec.code, + 'adUnitCode': 'adunit-code', + 'sizes': [[640, 480]], + 'params': { + 'publisherId': 'test-publisher-id' + }, + 'bidId': '5wfg9887sd5478', + 'bidderRequestId': 'op87952ewq8567', + 'auctionId': '87se98rt-5789-8735-2546-t98yh5678231', + 'mediaTypes': { + 'video': { + 'playerSize': [[640, 480]], + 'context': 'instream' + } + }, + 'vastXml': '"..."' + }, + { + 'bidder': spec.code, + 'adUnitCode': 'adunit-code', + 'sizes': [[300, 250]], + 'params': { + 'publisherId': 'test-publisher-id' + }, + 'bidId': '5wfg9887sd5478', + 'bidderRequestId': 'op87952ewq8567', + 'auctionId': '87se98rt-5789-8735-2546-t98yh5678231', + 'mediaTypes': { + 'banner': { + } + }, + 'ad': '""' + } + ]; + + const testModeBidRequests = [ + { + 'bidder': spec.code, + 'adUnitCode': 'adunit-code', + 'sizes': [[640, 480]], + 'params': { + 'publisherId': 'test-publisher-id', + 'testMode': true + }, + 'bidId': '5wfg9887sd5478', + 'bidderRequestId': 'op87952ewq8567', + 'auctionId': '87se98rt-5789-8735-2546-t98yh5678231', + } + ]; + + const bidderRequest = { + bidderCode: 'kueez', + } + const placementId = '12345678'; + + 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); + }); + + it('sends bid request to ENDPOINT via POST', function () { + const request = spec.buildRequests(bidRequests, bidderRequest); + expect(request.url).to.equal(ENDPOINT); + expect(request.method).to.equal('POST'); + }); + + it('sends bid request to TEST ENDPOINT via POST', function () { + const request = spec.buildRequests(testModeBidRequests, bidderRequest); + expect(request.url).to.equal(TEST_ENDPOINT); + expect(request.method).to.equal('POST'); + }); + + it('should send the correct bid Id', function () { + const request = spec.buildRequests(bidRequests, bidderRequest); + expect(request.data.bids[0].bidId).to.equal('5wfg9887sd5478'); + }); + + it('should send the correct sizes array', function () { + const request = spec.buildRequests(bidRequests, bidderRequest); + expect(request.data.bids[0].sizes).to.be.an('array'); + 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) + }); + + 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) + }); + + it('should respect syncEnabled option', function() { + config.setConfig({ + userSync: { + syncEnabled: false, + filterSettings: { + all: { + bidders: '*', + filter: 'include' + } + } + } + }); + const request = spec.buildRequests(bidRequests, bidderRequest); + expect(request.data.params).to.be.an('object'); + expect(request.data.params).to.not.have.property('cs_method'); + }); + + it('should respect "iframe" filter settings', function () { + config.setConfig({ + userSync: { + syncEnabled: true, + filterSettings: { + iframe: { + bidders: [spec.code], + filter: 'include' + } + } + } + }); + const request = spec.buildRequests(bidRequests, bidderRequest); + expect(request.data.params).to.be.an('object'); + expect(request.data.params).to.have.property('cs_method', 'iframe'); + }); + + it('should respect "all" filter settings', function () { + config.setConfig({ + userSync: { + syncEnabled: true, + filterSettings: { + all: { + bidders: [spec.code], + filter: 'include' + } + } + } + }); + const request = spec.buildRequests(bidRequests, bidderRequest); + expect(request.data.params).to.be.an('object'); + expect(request.data.params).to.have.property('cs_method', 'iframe'); + }); + + it('should send the pixel user sync param if userSync is enabled and no "iframe" or "all" configs are present', function () { + config.resetConfig(); + config.setConfig({ + userSync: { + syncEnabled: true, + } + }); + const request = spec.buildRequests(bidRequests, bidderRequest); + expect(request.data.params).to.be.an('object'); + expect(request.data.params).to.have.property('cs_method', 'pixel'); + }); + + it('should respect total exclusion', function() { + config.setConfig({ + userSync: { + syncEnabled: true, + filterSettings: { + image: { + bidders: [spec.code], + filter: 'exclude' + }, + iframe: { + bidders: [spec.code], + filter: 'exclude' + } + } + } + }); + const request = spec.buildRequests(bidRequests, bidderRequest); + expect(request.data.params).to.be.an('object'); + expect(request.data.params).to.not.have.property('cs_method'); + }); + + it('should have us_privacy param if usPrivacy is available in the bidRequest', function () { + const bidderRequestWithUSP = Object.assign({uspConsent: '1YNN'}, bidderRequest); + const request = spec.buildRequests(bidRequests, bidderRequestWithUSP); + expect(request.data.params).to.be.an('object'); + expect(request.data.params).to.have.property('us_privacy', '1YNN'); + }); + + it('should have an empty us_privacy param if usPrivacy is missing in the bidRequest', function () { + const request = spec.buildRequests(bidRequests, bidderRequest); + expect(request.data.params).to.be.an('object'); + expect(request.data.params).to.not.have.property('us_privacy'); + }); + + it('should not send the gdpr param if gdprApplies is false in the bidRequest', function () { + const bidderRequestWithGDPR = Object.assign({gdprConsent: {gdprApplies: false}}, bidderRequest); + const request = spec.buildRequests(bidRequests, bidderRequestWithGDPR); + expect(request.data.params).to.be.an('object'); + expect(request.data.params).to.not.have.property('gdpr'); + expect(request.data.params).to.not.have.property('gdpr_consent'); + }); + + it('should send the gdpr param if gdprApplies is true in the bidRequest', function () { + const bidderRequestWithGDPR = Object.assign({gdprConsent: {gdprApplies: true, consentString: 'test-consent-string'}}, bidderRequest); + const request = spec.buildRequests(bidRequests, bidderRequestWithGDPR); + expect(request.data.params).to.be.an('object'); + expect(request.data.params).to.have.property('gdpr', true); + expect(request.data.params).to.have.property('gdpr_consent', 'test-consent-string'); + }); + + it('should have schain param if it is available in the bidRequest', () => { + const schain = { + ver: '1.0', + complete: 1, + nodes: [{ asi: 'indirectseller.com', sid: '00001', hp: 1 }], + }; + bidRequests[0].schain = schain; + const request = spec.buildRequests(bidRequests, bidderRequest); + expect(request.data.params).to.be.an('object'); + expect(request.data.params).to.have.property('schain', '1.0,1!indirectseller.com,00001,1,,,'); + }); + + it('should set flooPrice to getFloor.floor value if it is greater than params.floorPrice', function() { + const bid = utils.deepClone(bidRequests[0]); + bid.getFloor = () => { + return { + currency: 'USD', + floor: 3.32 + } + } + bid.params.floorPrice = 0.64; + const request = spec.buildRequests([bid], bidderRequest); + expect(request.data.bids[0]).to.be.an('object'); + expect(request.data.bids[0]).to.have.property('floorPrice', 3.32); + }); + + it('should set floorPrice to params.floorPrice value if it is greater than getFloor.floor', function() { + const bid = utils.deepClone(bidRequests[0]); + bid.getFloor = () => { + return { + currency: 'USD', + floor: 0.8 + } + } + bid.params.floorPrice = 1.5; + const request = spec.buildRequests([bid], bidderRequest); + expect(request.data.bids[0]).to.be.an('object'); + expect(request.data.bids[0]).to.have.property('floorPrice', 1.5); + }); + }); + + describe('interpretResponse', function () { + const response = { + params: { + currency: 'USD', + netRevenue: true, + }, + bids: [{ + cpm: 12.5, + vastXml: '', + width: 640, + height: 480, + requestId: '21e12606d47ba7', + adomain: ['abc.com'], + mediaType: VIDEO + }, + { + cpm: 12.5, + ad: '""', + width: 300, + height: 250, + requestId: '21e12606d47ba7', + adomain: ['abc.com'], + mediaType: BANNER + }] + }; + + const expectedVideoResponse = { + cpm: 12.5, + creativeId: '21e12606d47ba7', + currency: 'USD', + height: 480, + mediaType: VIDEO, + meta: { + mediaType: VIDEO, + advertiserDomains: ['abc.com'] + }, + netRevenue: true, + nurl: 'http://example.com/win/1234', + requestId: '21e12606d47ba7', + ttl: TTL, + width: 640, + vastXml: '' + }; + + const expectedBannerResponse = { + cpm: 12.5, + creativeId: '21e12606d47ba7', + currency: 'USD', + height: 480, + mediaType: BANNER, + meta: { + mediaType: BANNER, + advertiserDomains: ['abc.com'] + }, + netRevenue: true, + nurl: 'http://example.com/win/1234', + requestId: '21e12606d47ba7', + ttl: TTL, + width: 640, + ad: '""' + }; + + 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)); + }); + + it('video type should have vastXml key', function () { + const result = spec.interpretResponse({ body: response }); + expect(result[0].vastXml).to.equal(expectedVideoResponse.vastXml) + }); + + it('banner type should have ad key', function () { + const result = spec.interpretResponse({ body: response }); + expect(result[1].ad).to.equal(expectedBannerResponse.ad) + }); + }) + + describe('getUserSyncs', function() { + const imageSyncResponse = { + body: { + params: { + userSyncPixels: [ + 'https://image-sync-url.test/1', + 'https://image-sync-url.test/2', + 'https://image-sync-url.test/3' + ] + } + } + }; + + const iframeSyncResponse = { + body: { + params: { + userSyncURL: 'https://iframe-sync-url.test' + } + } + }; + + it('should register all img urls from the response', function() { + const syncs = spec.getUserSyncs({ pixelEnabled: true }, [imageSyncResponse]); + expect(syncs).to.deep.equal([ + { + type: 'image', + url: 'https://image-sync-url.test/1' + }, + { + type: 'image', + url: 'https://image-sync-url.test/2' + }, + { + type: 'image', + url: 'https://image-sync-url.test/3' + } + ]); + }); + + it('should register the iframe url from the response', function() { + const syncs = spec.getUserSyncs({ iframeEnabled: true }, [iframeSyncResponse]); + expect(syncs).to.deep.equal([ + { + type: 'iframe', + url: 'https://iframe-sync-url.test' + } + ]); + }); + + it('should register both image and iframe urls from the responses', function() { + const syncs = spec.getUserSyncs({ pixelEnabled: true, iframeEnabled: true }, [iframeSyncResponse, imageSyncResponse]); + expect(syncs).to.deep.equal([ + { + type: 'iframe', + url: 'https://iframe-sync-url.test' + }, + { + type: 'image', + url: 'https://image-sync-url.test/1' + }, + { + type: 'image', + url: 'https://image-sync-url.test/2' + }, + { + type: 'image', + url: 'https://image-sync-url.test/3' + } + ]); + }); + + it('should handle an empty response', function() { + const syncs = spec.getUserSyncs({ iframeEnabled: true }, []); + expect(syncs).to.deep.equal([]); + }); + + it('should handle when user syncs are disabled', function() { + const syncs = spec.getUserSyncs({ pixelEnabled: false }, [imageSyncResponse]); + expect(syncs).to.deep.equal([]); + }); + }) + + describe('onBidWon', function() { + beforeEach(function() { + sinon.stub(utils, 'triggerPixel'); + }); + afterEach(function() { + utils.triggerPixel.restore(); + }); + + it('Should trigger pixel if bid nurl', function() { + const bid = { + 'bidder': spec.code, + 'adUnitCode': 'adunit-code', + 'sizes': [['640', '480']], + 'nurl': 'http://example.com/win/1234', + 'params': { + 'publisherId': 'test-publisher-id' + } + }; + + spec.onBidWon(bid); + expect(utils.triggerPixel.callCount).to.equal(1) + }) + }) +}); From 988cf28f859caa70b09fd43f3523a19363246dab Mon Sep 17 00:00:00 2001 From: Laslo Chechur Date: Wed, 8 Jun 2022 17:02:19 +0300 Subject: [PATCH 13/15] change publisherId to org --- modules/kueezBidAdapter.js | 6 +++--- modules/kueezBidAdapter.md | 2 +- test/spec/modules/kueezBidAdapter_spec.js | 12 ++++++------ 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/modules/kueezBidAdapter.js b/modules/kueezBidAdapter.js index 73265c8df4f..295be160ee9 100644 --- a/modules/kueezBidAdapter.js +++ b/modules/kueezBidAdapter.js @@ -299,7 +299,7 @@ function generateSharedParams(sharedParams, bidderRequest) { auction_start: timestamp(), device_type: getDeviceType(), dnt: (navigator.doNotTrack === 'yes' || navigator.doNotTrack === '1' || navigator.msDoNotTrack === '1') ? 1 : 0, - publisher_id: generalBidParams.publisherId, + publisher_id: generalBidParams.org, publisher_name: domain, session_id: getBidIdParameter('auctionId', sharedParams), site_domain: domain, @@ -367,8 +367,8 @@ function validateParams(bidRequest) { isValid = false; } - if (!bidRequest.params.publisherId) { - logWarn('Kueez adapter - publisherId is a required param'); + if (!bidRequest.params.org) { + logWarn('Kueez adapter - org is a required param'); isValid = false; } diff --git a/modules/kueezBidAdapter.md b/modules/kueezBidAdapter.md index 49c737f6b39..460cd52c268 100644 --- a/modules/kueezBidAdapter.md +++ b/modules/kueezBidAdapter.md @@ -18,7 +18,7 @@ The adapter supports Banner and Video(instream) media types. | Name | Scope | Type | Description | Example |---------------| ----- | ---- |-------------------------------------------------------------------| ------- -| `publisherId` | required | String | the publisher Id provided by your Kueez representative | "test-publisher-id" +| `org` | required | String | the organization Id provided by your Kueez representative | "test-publisher-id" | `floorPrice` | optional | Number | Minimum price in USD. Misuse of this parameter can impact revenue | 1.50 | `placementId` | optional | String | A unique placement identifier | "12345678" | `testMode` | optional | Boolean | This activates the test mode | false diff --git a/test/spec/modules/kueezBidAdapter_spec.js b/test/spec/modules/kueezBidAdapter_spec.js index b7c78d48426..16440965765 100644 --- a/test/spec/modules/kueezBidAdapter_spec.js +++ b/test/spec/modules/kueezBidAdapter_spec.js @@ -25,7 +25,7 @@ describe('kueezBidAdapter', function () { 'adUnitCode': 'adunit-code', 'sizes': [['640', '480']], 'params': { - 'publisherId': 'test-publisher-id' + 'org': 'test-publisher-id' } }; @@ -37,7 +37,7 @@ describe('kueezBidAdapter', function () { const newBid = Object.assign({}, bid); delete newBid.params; newBid.params = { - 'publisherId': null + 'org': null }; expect(spec.isBidRequestValid(newBid)).to.equal(false); }); @@ -50,7 +50,7 @@ describe('kueezBidAdapter', function () { 'adUnitCode': 'adunit-code', 'sizes': [[640, 480]], 'params': { - 'publisherId': 'test-publisher-id' + 'org': 'test-publisher-id' }, 'bidId': '5wfg9887sd5478', 'bidderRequestId': 'op87952ewq8567', @@ -68,7 +68,7 @@ describe('kueezBidAdapter', function () { 'adUnitCode': 'adunit-code', 'sizes': [[300, 250]], 'params': { - 'publisherId': 'test-publisher-id' + 'org': 'test-publisher-id' }, 'bidId': '5wfg9887sd5478', 'bidderRequestId': 'op87952ewq8567', @@ -87,7 +87,7 @@ describe('kueezBidAdapter', function () { 'adUnitCode': 'adunit-code', 'sizes': [[640, 480]], 'params': { - 'publisherId': 'test-publisher-id', + 'org': 'test-publisher-id', 'testMode': true }, 'bidId': '5wfg9887sd5478', @@ -468,7 +468,7 @@ describe('kueezBidAdapter', function () { 'sizes': [['640', '480']], 'nurl': 'http://example.com/win/1234', 'params': { - 'publisherId': 'test-publisher-id' + 'org': 'test-publisher-id' } }; From f415ca29396a78f3e94c88ab4134b27cd8e5cdc6 Mon Sep 17 00:00:00 2001 From: Laslo Chechur Date: Sun, 12 Jun 2022 14:45:58 +0300 Subject: [PATCH 14/15] update the kueez documentation --- modules/kueezBidAdapter.md | 66 ++++++++++++++++++++++++++------------ 1 file changed, 45 insertions(+), 21 deletions(-) diff --git a/modules/kueezBidAdapter.md b/modules/kueezBidAdapter.md index 460cd52c268..feb0b20a2a1 100644 --- a/modules/kueezBidAdapter.md +++ b/modules/kueezBidAdapter.md @@ -6,7 +6,6 @@ Module Type: Bidder Adapter Maintainer: prebid@kueez.com - # Description The Kueez adapter requires setup and approval from the Kueez team. Please reach out to prebid@kueez.com for more information. @@ -14,6 +13,7 @@ The Kueez adapter requires setup and approval from the Kueez team. Please reach The adapter supports Banner and Video(instream) media types. # Bid Parameters + ## Video | Name | Scope | Type | Description | Example @@ -24,26 +24,50 @@ The adapter supports Banner and Video(instream) media types. | `testMode` | optional | Boolean | This activates the test mode | false # Test Parameters + ```javascript -var adUnits = [ - { - code: 'dfp-video-div', - sizes: [[640, 480]], - mediaTypes: { - video: { - playerSize: [[640, 480]], - context: 'instream' - } - }, - bids: [{ - bidder: 'kueez', - params: { - org: 'test-publisher-id', // Required - floorPrice: 1.50, // Optional - placementId: '12345678', // Optional - testMode: false // Optional - } - }] +var adUnits = [{ + code: 'banner-div', + mediaTypes: { + banner: { + sizes: [ + [300, 250], + [728, 90] + ] + } + }, + bids: [{ + bidder: 'kueez', + params: { + org: 'test-org-id', // Required + floorPrice: 0.10, // Optional + placementId: '12345678', // Optional + testMode: false // Optional + } + }] +}, + { + code: 'dfp-video-div', + sizes: [ + [640, 480] + ], + mediaTypes: { + video: { + playerSize: [ + [640, 480] + ], + context: 'instream' + } + }, + bids: [{ + bidder: 'kueez', + params: { + org: 'test-org-id', // Required + floorPrice: 2.00, // Optional + placementId: '12345678', // Optional + testMode: false // Optional } - ]; + }] + } +]; ``` From cfa2ddf607dbfdbd8676b183c3f4a5ec090c7f20 Mon Sep 17 00:00:00 2001 From: Laslo Chechur Date: Mon, 13 Jun 2022 10:00:04 +0300 Subject: [PATCH 15/15] update test config for kueez --- modules/kueezBidAdapter.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/kueezBidAdapter.md b/modules/kueezBidAdapter.md index feb0b20a2a1..c2afbf0798c 100644 --- a/modules/kueezBidAdapter.md +++ b/modules/kueezBidAdapter.md @@ -42,7 +42,7 @@ var adUnits = [{ org: 'test-org-id', // Required floorPrice: 0.10, // Optional placementId: '12345678', // Optional - testMode: false // Optional + testMode: true // Optional } }] }, @@ -65,7 +65,7 @@ var adUnits = [{ org: 'test-org-id', // Required floorPrice: 2.00, // Optional placementId: '12345678', // Optional - testMode: false // Optional + testMode: true // Optional } }] }