From 85e2a4a9504add0d626eb2d4bf2772e781963fdb Mon Sep 17 00:00:00 2001 From: SublimeJeremy Date: Fri, 11 Sep 2020 09:34:08 +0200 Subject: [PATCH] add sublimeBundleBidAdapter duplicated from sublimeBidAdapter + fix tests --- modules/sublimeBidAdapter.js | 6 +- modules/sublimeBundleBidAdapter.js | 282 ++++++++++++++++++ modules/sublimeBundleBidAdapter.md | 62 ++++ test/spec/modules/sublimeBidAdapter_spec.js | 10 +- .../modules/sublimeBundleBidAdapter_spec.js | 279 +++++++++++++++++ 5 files changed, 631 insertions(+), 8 deletions(-) create mode 100644 modules/sublimeBundleBidAdapter.js create mode 100644 modules/sublimeBundleBidAdapter.md create mode 100644 test/spec/modules/sublimeBundleBidAdapter_spec.js diff --git a/modules/sublimeBidAdapter.js b/modules/sublimeBidAdapter.js index 3def8100d49..a929fe21d9d 100644 --- a/modules/sublimeBidAdapter.js +++ b/modules/sublimeBidAdapter.js @@ -3,7 +3,6 @@ import { config } from '../src/config.js'; import * as utils from '../src/utils.js'; const BIDDER_CODE = 'sublime'; -const BIDDER_GVLID = 114; const DEFAULT_BID_HOST = 'pbjs.sskzlabs.com'; const DEFAULT_CALLBACK_NAME = 'sublime_prebid_callback'; const DEFAULT_CURRENCY = 'EUR'; @@ -270,13 +269,14 @@ function onTimeout(timeoutData) { } export const spec = { - code: BIDDER_CODE, aliases: [], - isBidRequestValid: isBidRequestValid, buildRequests: buildRequests, + code: BIDDER_CODE, interpretResponse: interpretResponse, + isBidRequestValid: isBidRequestValid, onBidWon: onBidWon, onTimeout: onTimeout, + sendEvent: sendEvent, }; registerBidder(spec); diff --git a/modules/sublimeBundleBidAdapter.js b/modules/sublimeBundleBidAdapter.js new file mode 100644 index 00000000000..a929fe21d9d --- /dev/null +++ b/modules/sublimeBundleBidAdapter.js @@ -0,0 +1,282 @@ +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { config } from '../src/config.js'; +import * as utils from '../src/utils.js'; + +const BIDDER_CODE = 'sublime'; +const DEFAULT_BID_HOST = 'pbjs.sskzlabs.com'; +const DEFAULT_CALLBACK_NAME = 'sublime_prebid_callback'; +const DEFAULT_CURRENCY = 'EUR'; +const DEFAULT_PROTOCOL = 'https'; +const DEFAULT_SAC_HOST = 'sac.ayads.co' +const DEFAULT_TTL = 600; +const SUBLIME_ANTENNA = 'antenna.ayads.co'; +const SUBLIME_VERSION = '0.5.2-bundle'; + +/** + * Debug log message + * @param {String} msg + * @param {Object} obj + */ +function log(msg, obj) { + utils.logInfo('SublimeBidAdapter - ' + msg, obj); +} + +// Default state +const state = { + zoneId: '', + transactionId: '' +}; + +/** + * Set a new state + * @param {Object} value + */ +function setState(value) { + Object.assign(state, value); + log('State has been updated :', state); +} + +/** + * Send pixel to our debug endpoint + * @param {string} eventName - Event name that will be send in the e= query string + * @param {Boolean} isMandatoryPixel - If set to true, will always send the pixel + */ +function sendEvent(eventName, isMandatoryPixel = false) { + let shoudSendPixel = (isMandatoryPixel || state.debug); + let ts = Date.now(); + let eventObject = { + t: ts, + tse: ts, + z: state.zoneId, + e: eventName, + src: 'pa', + puid: state.transactionId, + trId: state.transactionId, + ver: SUBLIME_VERSION, + }; + + if (shoudSendPixel) { + log('Sending pixel for event: ' + eventName, eventObject); + + let queryString = utils.formatQS(eventObject); + utils.triggerPixel('https://' + SUBLIME_ANTENNA + '/?' + queryString); + } else { + log('Not sending pixel for event (use debug: true to send it): ' + eventName, eventObject); + } +} + +/** + * Determines whether or not the given bid request is valid. + * + * @param {BidRequest} bid The bid params to validate. + * @return {Boolean} True if this is a valid bid, and false otherwise. + */ +function isBidRequestValid(bid) { + return !!Number(bid.params.zoneId); +} + +/** + * Make a server request from the list of BidRequests. + * + * @param {BidRequest[]} validBidRequests - An array of bids + * @param {Object} bidderRequest - Info describing the request to the server. + * @return {ServerRequest|ServerRequest[]} - Info describing the request to the server. + */ +function buildRequests(validBidRequests, bidderRequest) { + window.sublime = window.sublime || {}; + + let commonPayload = { + pbav: SUBLIME_VERSION, + // Current Prebid params + prebidVersion: '$prebid.version$', + currencyCode: config.getConfig('currency.adServerCurrency') || DEFAULT_CURRENCY, + timeout: (typeof bidderRequest === 'object' && !!bidderRequest) ? bidderRequest.timeout : config.getConfig('bidderTimeout'), + }; + + // RefererInfo + if (bidderRequest && bidderRequest.refererInfo) { + commonPayload.referer = bidderRequest.refererInfo.referer; + commonPayload.numIframes = bidderRequest.refererInfo.numIframes; + } + // GDPR handling + if (bidderRequest && bidderRequest.gdprConsent) { + commonPayload.gdprConsent = bidderRequest.gdprConsent.consentString; + commonPayload.gdpr = bidderRequest.gdprConsent.gdprApplies; // we're handling the undefined case server side + + // Injecting gdpr consent into sublime tag + window.sublime.gdpr = window.sublime.gdpr || {}; + window.sublime.gdpr.injected = { + consentString: bidderRequest.gdprConsent.consentString, + gdprApplies: bidderRequest.gdprConsent.gdprApplies + }; + } + + return validBidRequests.filter((bid, index) => { + // Any bidRequest after the first is skipped + if (index) { + let leftoverZonesIds = validBidRequests.slice(1).map(bid => { return bid.params.zoneId }).join(','); + utils.logWarn(`SublimeBidAdapter - ZoneIds ${leftoverZonesIds} are ignored. Only one ZoneId per page can be instanciated.`); + return false; + } + + return bid; + }).map(bid => { + let bidHost = bid.params.bidHost || DEFAULT_BID_HOST; + let callbackName = (bid.params.callbackName || DEFAULT_CALLBACK_NAME) + '_' + bid.params.zoneId; + let protocol = bid.params.protocol || DEFAULT_PROTOCOL; + let sacHost = bid.params.sacHost || DEFAULT_SAC_HOST; + + setState({ + transactionId: bid.transactionId, + zoneId: bid.params.zoneId, + debug: bid.params.debug || false, + }); + + // Adding Sublime tag + let script = document.createElement('script'); + script.type = 'application/javascript'; + script.src = 'https://' + sacHost + '/sublime/' + bid.params.zoneId + '/prebid?callback=' + callbackName; + document.body.appendChild(script); + + // Register a callback to send notify + window[callbackName] = (response) => { + let xhr = new XMLHttpRequest(); + let hasAd = response.ad ? '1' : '0'; + let url = protocol + '://' + bidHost + '/notify'; + + let params = { + a: hasAd, + ad: response.ad || '', + cpm: response.cpm || 0, + currency: response.currency || DEFAULT_CURRENCY, + notify: 1, + requestId: bid.bidId ? encodeURIComponent(bid.bidId) : null, + transactionId: bid.transactionId, + zoneId: bid.params.zoneId + }; + + let queryString = Object.keys(params).map(key => { + return key + '=' + encodeURIComponent(params[key]) + }).join('&'); + + xhr.open('POST', url, true); + xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); + xhr.send(queryString); + return xhr; + }; + + let bidPayload = { + adUnitCode: bid.adUnitCode, + auctionId: bid.auctionId, + bidder: bid.bidder, + bidderRequestId: bid.bidderRequestId, + bidRequestsCount: bid.bidRequestsCount, + requestId: bid.bidId, + sizes: bid.sizes.map(size => ({ + w: size[0], + h: size[1], + })), + transactionId: bid.transactionId, + zoneId: bid.params.zoneId, + }; + + let payload = Object.assign({}, commonPayload, bidPayload); + + return { + method: 'POST', + url: protocol + '://' + bidHost + '/bid', + data: payload, + options: { + contentType: 'application/json', + withCredentials: true + }, + } + }); +} + +/** + * Unpack the response from the server into a list of bids. + * + * @param {*} serverResponse A successful response from the server. + * @param {*} bidRequest An object with bid request informations + * @return {Bid[]} An array of bids which were nested inside the server. + */ +function interpretResponse(serverResponse, bidRequest) { + const bidResponses = []; + const response = serverResponse.body; + + sendEvent('dintres'); + + if (response) { + if (response.timeout || !response.ad || //gmi.test(response.ad)) { + return bidResponses; + } + + // Setting our returned sizes object to default values + let returnedSizes = { + width: 1800, + height: 1000 + }; + + // Verifying Banner sizes + if (bidRequest && bidRequest.data && bidRequest.data.w === 1 && bidRequest.data.h === 1) { + // If banner sizes are 1x1 we set our default size object to 1x1 + returnedSizes = { + width: 1, + height: 1 + }; + } + + const bidResponse = { + requestId: response.requestId || '', + cpm: response.cpm || 0, + width: response.width || returnedSizes.width, + height: response.height || returnedSizes.height, + creativeId: response.creativeId || 1, + dealId: response.dealId || 1, + currency: response.currency || DEFAULT_CURRENCY, + netRevenue: response.netRevenue || true, + ttl: response.ttl || DEFAULT_TTL, + ad: response.ad, + pbav: SUBLIME_VERSION + }; + + sendEvent('bida', true); + bidResponses.push(bidResponse); + } else { + sendEvent('dnobid'); + } + + return bidResponses; +} + +/** + * Send pixel when bidWon event is triggered + * @param {Object} timeoutData + */ +function onBidWon(bid) { + log('Bid won', bid); + sendEvent('bidwon', true); +} + +/** + * Send debug when we timeout + * @param {Object} timeoutData + */ +function onTimeout(timeoutData) { + log('Timeout from adapter', timeoutData); + sendEvent('dbidtimeout', true); +} + +export const spec = { + aliases: [], + buildRequests: buildRequests, + code: BIDDER_CODE, + interpretResponse: interpretResponse, + isBidRequestValid: isBidRequestValid, + onBidWon: onBidWon, + onTimeout: onTimeout, + sendEvent: sendEvent, +}; + +registerBidder(spec); diff --git a/modules/sublimeBundleBidAdapter.md b/modules/sublimeBundleBidAdapter.md new file mode 100644 index 00000000000..1af39e6d091 --- /dev/null +++ b/modules/sublimeBundleBidAdapter.md @@ -0,0 +1,62 @@ +# Overview + +``` +Module Name: Sublime Bundle Bid Adapter +Module Type: Bidder Adapter +Maintainer: pbjs@sublimeskinz.com +``` + +# Description + +Connects to Sublime for bids. +Sublime bid adapter supports Skinz. + +# Nota Bene + +Our prebid adapter is unusable with SafeFrame. + +# Build + +You can build your version of prebid.js, execute: + +```shell +gulp build --modules=sublimeBundleBidAdapter +``` + +Or to build with multiple adapters + +```shell +gulp build --modules=sublimeBundleBidAdapter,secondAdapter,thirdAdapter +``` + +More details in the root [README](../README.md#Build) + +## To build from you own repository + +- copy `/modules/sublimeBundleBidAdapter.js` to your `/modules/` directory +- copy `/modules/sublimeBundleBidAdapter.md` to your `/modules/` directory +- copy `/test/spec/modules/sublimeBundleBidAdapter_spec.js` to your `/test/spec/modules/` directory + +Then build + + +# Invocation Parameters + +```js +var adUnits = [{ + code: 'sublime', + mediaTypes: { + banner: { + sizes: [1800, 1000] + } + }, + bids: [{ + bidder: 'sublime', + params: { + zoneId: + } + }] +}]; +``` + +Where you replace `` by your Sublime Zone id diff --git a/test/spec/modules/sublimeBidAdapter_spec.js b/test/spec/modules/sublimeBidAdapter_spec.js index ab070a5855c..d8330883179 100644 --- a/test/spec/modules/sublimeBidAdapter_spec.js +++ b/test/spec/modules/sublimeBidAdapter_spec.js @@ -1,5 +1,5 @@ import { expect } from 'chai'; -import { spec, sendEvent, log, setState, state } from 'modules/sublimeBidAdapter.js'; +import { spec } from 'modules/sublimeBidAdapter.js'; import { newBidder } from 'src/adapters/bidderFactory.js'; let utils = require('src/utils'); @@ -16,7 +16,7 @@ describe('Sublime Adapter', function() { it('should trigger pixel', function () { sandbox.spy(utils, 'triggerPixel'); - sendEvent('test', true); + spec.sendEvent('test', true); expect(utils.triggerPixel.called).to.equal(true); }); @@ -146,7 +146,7 @@ describe('Sublime Adapter', function() { currency: 'USD', netRevenue: true, ttl: 600, - pbav: '0.5.2', + pbav: '0.5.2-bundle', ad: '', }, ]; @@ -188,7 +188,7 @@ describe('Sublime Adapter', function() { netRevenue: true, ttl: 600, ad: '', - pbav: '0.5.2', + pbav: '0.5.2-bundle', }; expect(result[0]).to.deep.equal(expectedResponse); @@ -238,7 +238,7 @@ describe('Sublime Adapter', function() { netRevenue: true, ttl: 600, ad: '', - pbav: '0.5.2', + pbav: '0.5.2-bundle', }; expect(result[0]).to.deep.equal(expectedResponse); diff --git a/test/spec/modules/sublimeBundleBidAdapter_spec.js b/test/spec/modules/sublimeBundleBidAdapter_spec.js new file mode 100644 index 00000000000..caa6f17d2aa --- /dev/null +++ b/test/spec/modules/sublimeBundleBidAdapter_spec.js @@ -0,0 +1,279 @@ +import { expect } from 'chai'; +import { spec } from 'modules/sublimeBundleBidAdapter.js'; +import { newBidder } from 'src/adapters/bidderFactory.js'; + +let utils = require('src/utils'); + +describe('Sublime Adapter', function() { + const adapter = newBidder(spec); + + describe('sendEvent', function() { + let sandbox; + + beforeEach(function () { + sandbox = sinon.sandbox.create(); + }); + + it('should trigger pixel', function () { + sandbox.spy(utils, 'triggerPixel'); + spec.sendEvent('test', true); + expect(utils.triggerPixel.called).to.equal(true); + }); + + afterEach(function () { + sandbox.restore(); + }); + }) + + describe('inherited functions', function() { + it('exists and is a function', function() { + expect(adapter.callBids).to.exist.and.to.be.a('function'); + }); + }); + + describe('isBidRequestValid', function() { + let bid = { + bidder: 'sublime', + params: { + zoneId: 24549, + endpoint: '', + }, + }; + + it('should return true when required params found', function() { + expect(spec.isBidRequestValid(bid)).to.equal(true); + }); + + it('should return false when required params are not passed', function() { + let bid = Object.assign({}, bid); + bid.params = {}; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + }); + + describe('buildRequests', function() { + let bidRequests = [ + { + bidder: 'sublime', + adUnitCode: 'sublime_code', + bidId: 'abc1234', + sizes: [[1800, 1000], [640, 300]], + requestId: 'xyz654', + params: { + zoneId: 123, + callbackName: 'false' + } + }, { + bidder: 'sublime', + adUnitCode: 'sublime_code_2', + bidId: 'abc1234_2', + sizes: [[1, 1]], + requestId: 'xyz654_2', + params: { + zoneId: 456, + } + } + ]; + + let bidderRequest = { + gdprConsent: { + consentString: 'EOHEIRCOUCOUIEHZIOEIU-TEST', + gdprApplies: true + }, + refererInfo: { + referer: 'https://example.com', + numIframes: 2, + } + }; + + let request = spec.buildRequests(bidRequests, bidderRequest); + + it('should have a post method', function() { + expect(request[0].method).to.equal('POST'); + }); + + it('should contains a request id equals to the bid id', function() { + expect(request[0].data.requestId).to.equal(bidRequests[0].bidId); + }); + + it('should have an url that contains bid keyword', function() { + expect(request[0].url).to.match(/bid/); + }); + }); + + describe('buildRequests: default arguments', function() { + let bidRequests = [{ + bidder: 'sublime', + adUnitCode: 'sublime_code', + bidId: 'abc1234', + sizes: [[1800, 1000], [640, 300]], + requestId: 'xyz654', + params: { + zoneId: 123 + } + }]; + + let request = spec.buildRequests(bidRequests); + + it('should have an url that match the default endpoint', function() { + expect(request[0].url).to.equal('https://pbjs.sskzlabs.com/bid'); + }); + }); + + describe('interpretResponse', function() { + let serverResponse = { + 'request_id': '3db3773286ee59', + 'cpm': 0.5, + 'ad': '', + }; + + it('should get correct bid response', function() { + // Mock the fire method + top.window.sublime = { + analytics: { + fire: function() {} + } + }; + + let expectedResponse = [ + { + requestId: '', + cpm: 0.5, + width: 1800, + height: 1000, + creativeId: 1, + dealId: 1, + currency: 'USD', + netRevenue: true, + ttl: 600, + pbav: '0.5.2-bundle', + ad: '', + }, + ]; + let result = spec.interpretResponse({body: serverResponse}); + expect(Object.keys(result[0])).to.have.members(Object.keys(expectedResponse[0])); + }); + + it('should get correct default size for 1x1', function() { + let serverResponse = { + 'requestId': 'xyz654_2', + 'cpm': 0.5, + 'ad': '', + }; + + let bidRequest = { + bidder: 'sublime', + adUnitCode: 'sublime_code_2', + bidId: 'abc1234_2', + data: { + w: 1, + h: 1, + }, + requestId: 'xyz654_2', + params: { + zoneId: 456, + } + }; + + let result = spec.interpretResponse({body: serverResponse}, bidRequest); + + let expectedResponse = { + requestId: 'xyz654_2', + cpm: 0.5, + width: 1, + height: 1, + creativeId: 1, + dealId: 1, + currency: 'EUR', + netRevenue: true, + ttl: 600, + ad: '', + pbav: '0.5.2-bundle', + }; + + expect(result[0]).to.deep.equal(expectedResponse); + }); + + it('should return bid empty response', function () { + let serverResponse = ''; + let bidRequest = {}; + + let result = spec.interpretResponse({ body: serverResponse }, bidRequest); + + let expectedResponse = []; + + expect(result).to.deep.equal(expectedResponse); + }); + + it('should return bid with default value in response', function () { + let serverResponse = { + 'requestId': 'xyz654_2', + 'ad': '', + }; + + let bidRequest = { + bidder: 'sublime', + adUnitCode: 'sublime_code_2', + bidId: 'abc1234_2', + data: { + w: 1, + h: 1, + }, + requestId: 'xyz654_2', + params: { + zoneId: 456, + } + }; + + let result = spec.interpretResponse({ body: serverResponse }, bidRequest); + + let expectedResponse = { + requestId: 'xyz654_2', + cpm: 0, + width: 1, + height: 1, + creativeId: 1, + dealId: 1, + currency: 'EUR', + netRevenue: true, + ttl: 600, + ad: '', + pbav: '0.5.2-bundle', + }; + + expect(result[0]).to.deep.equal(expectedResponse); + }); + + it('should return empty bid response because of timeout', function () { + let serverResponse = { + 'requestId': 'xyz654_2', + 'timeout': true, + 'ad': '', + }; + + let bidRequest = { + bidder: 'sublime', + adUnitCode: 'sublime_code_2', + bidId: 'abc1234_2', + data: { + w: 1, + h: 1, + }, + requestId: 'xyz654_2', + params: { + zoneId: 456, + } + }; + + let result = spec.interpretResponse({ body: serverResponse }, bidRequest); + + let expectedResponse = []; + + expect(result).to.deep.equal(expectedResponse); + + describe('On bid Time out', function () { + spec.onTimeout(result); + }); + }); + }); +});