From c0ef18028ace3f7016066688f894f27be30858c5 Mon Sep 17 00:00:00 2001 From: Valentin Souche Date: Thu, 27 Sep 2018 10:08:36 +0200 Subject: [PATCH 1/2] Add teads bidder adapter --- modules/teadsBidAdapter.js | 135 ++++++++++++ modules/teadsBidAdapter.md | 48 ++++ test/spec/modules/teadsBidAdapter_spec.js | 254 ++++++++++++++++++++++ 3 files changed, 437 insertions(+) create mode 100644 modules/teadsBidAdapter.js create mode 100644 modules/teadsBidAdapter.md create mode 100644 test/spec/modules/teadsBidAdapter_spec.js diff --git a/modules/teadsBidAdapter.js b/modules/teadsBidAdapter.js new file mode 100644 index 00000000000..2beaa36c925 --- /dev/null +++ b/modules/teadsBidAdapter.js @@ -0,0 +1,135 @@ +import {registerBidder} from 'src/adapters/bidderFactory'; +const utils = require('src/utils'); +const BIDDER_CODE = 'teads'; +const ENDPOINT_URL = '//a.teads.tv/hb/bid-request'; +const gdprStatus = { + GDPR_APPLIES_PUBLISHER: 12, + GDPR_APPLIES_GLOBAL: 11, + GDPR_DOESNT_APPLY: 0, + CMP_NOT_FOUND_OR_ERROR: 22 +} + +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: ['video', 'banner'], + /** + * Determines whether or not the given bid request is valid. + * + * @param {BidRequest} bid The bid params to validate. + * @return boolean True if this is a valid bid, and false otherwise. + */ + isBidRequestValid: function(bid) { + let isValid = false; + if (typeof bid.params !== 'undefined') { + let isValidPlacementId = _validateId(utils.getValue(bid.params, 'placementId')); + let isValidPageId = _validateId(utils.getValue(bid.params, 'pageId')); + isValid = isValidPlacementId && isValidPageId; + } + + if (!isValid) { + utils.logError('Teads placementId and pageId parameters are required. Bid aborted.'); + } + return isValid; + }, + /** + * Make a server request from the list of BidRequests. + * + * @param {validBidRequests[]} an array of bids + * @return ServerRequest Info describing the request to the server. + */ + buildRequests: function(validBidRequests, bidderRequest) { + const bids = validBidRequests.map(buildRequestObject); + const payload = { + referrer: utils.getTopWindowUrl(), + data: bids, + deviceWidth: screen.width + }; + + let gdpr = bidderRequest.gdprConsent; + if (bidderRequest && gdpr) { + let isCmp = (typeof gdpr.gdprApplies === 'boolean') + let isConsentString = (typeof gdpr.consentString === 'string') + let status = isCmp ? findGdprStatus(gdpr.gdprApplies, gdpr.vendorData) : gdprStatus.CMP_NOT_FOUND_OR_ERROR + payload.gdpr_iab = { + consent: isConsentString ? gdpr.consentString : '', + status: status + }; + } + + const payloadString = JSON.stringify(payload); + return { + method: 'POST', + url: ENDPOINT_URL, + data: payloadString, + }; + }, + /** + * Unpack the response from the server into a list of bids. + * + * @param {*} serverResponse A successful response from the server. + * @return {Bid[]} An array of bids which were nested inside the server. + */ + interpretResponse: function(serverResponse, bidderRequest) { + const bidResponses = []; + serverResponse = serverResponse.body; + + if (serverResponse.responses) { + serverResponse.responses.forEach(function (bid) { + const bidResponse = { + bidderCode: spec.code, + cpm: bid.cpm, + width: bid.width, + height: bid.height, + currency: bid.currency, + netRevenue: true, + ttl: bid.ttl, + ad: bid.ad, + requestId: bid.bidId, + creativeId: bid.creativeId + }; + bidResponses.push(bidResponse); + }); + } + return bidResponses; + }, + + getUserSyncs: function(syncOptions, responses, gdprApplies) { + if (syncOptions.iframeEnabled) { + return [{ + type: 'iframe', + url: '//sync.teads.tv/iframe' + }]; + } + } +}; + +function findGdprStatus(gdprApplies, gdprData) { + let status = gdprStatus.GDPR_APPLIES_PUBLISHER; + + if (gdprApplies) { + if (gdprData.hasGlobalScope || gdprData.hasGlobalConsent) status = gdprStatus.GDPR_APPLIES_GLOBAL + } else status = gdprStatus.GDPR_DOESNT_APPLY + return status; +} + +function buildRequestObject(bid) { + const reqObj = {}; + let placementId = utils.getValue(bid.params, 'placementId'); + let pageId = utils.getValue(bid.params, 'pageId'); + + reqObj.sizes = utils.parseSizesInput(bid.sizes); + reqObj.bidId = utils.getBidIdParameter('bidId', bid); + reqObj.bidderRequestId = utils.getBidIdParameter('bidderRequestId', bid); + reqObj.placementId = parseInt(placementId, 10); + reqObj.pageId = parseInt(pageId, 10); + reqObj.adUnitCode = utils.getBidIdParameter('adUnitCode', bid); + reqObj.auctionId = utils.getBidIdParameter('auctionId', bid); + reqObj.transactionId = utils.getBidIdParameter('transactionId', bid); + return reqObj; +} + +function _validateId(id) { + return (parseInt(id) > 0); +} + +registerBidder(spec); diff --git a/modules/teadsBidAdapter.md b/modules/teadsBidAdapter.md new file mode 100644 index 00000000000..ded9323540b --- /dev/null +++ b/modules/teadsBidAdapter.md @@ -0,0 +1,48 @@ +# Overview + +**Module Name**: Teads Bidder Adapter +**Module Type**: Bidder Adapter +**Maintainer**: innov-ssp@teads.tv + +# Description + +Use `teads` as bidder. + +`placementId` & `pageId` are required and must be integers. + +## AdUnits configuration example +``` + var adUnits = [{ + code: 'your-slot_1-div', //use exactly the same code as your slot div id. + sizes: [[300, 250]], + bids: [{ + bidder: 'teads', + params: { + placementId: 12345, + pageId: 1234 + } + }] + },{ + code: 'your-slot_2-div', //use exactly the same code as your slot div id. + sizes: [[600, 800]], + bids: [{ + bidder: 'teads', + params: { + placementId: 12345, + pageId: 1234 + } + }] + }]; +``` + +## UserSync example + +``` +pbjs.setConfig({ + userSync: { + iframeEnabled: true, + syncEnabled: true, + syncDelay: 1 + } +}); +``` diff --git a/test/spec/modules/teadsBidAdapter_spec.js b/test/spec/modules/teadsBidAdapter_spec.js new file mode 100644 index 00000000000..6b5e08dab08 --- /dev/null +++ b/test/spec/modules/teadsBidAdapter_spec.js @@ -0,0 +1,254 @@ +import {expect} from 'chai'; +import {spec} from 'modules/teadsBidAdapter'; +import {newBidder} from 'src/adapters/bidderFactory'; + +const ENDPOINT = '//a.teads.tv/hb/bid-request'; +const AD_SCRIPT = '"'; + +describe('teadsBidAdapter', () => { + const adapter = newBidder(spec); + + describe('inherited functions', () => { + it('exists and is a function', () => { + expect(adapter.callBids).to.exist.and.to.be.a('function'); + }); + }); + + describe('isBidRequestValid', () => { + let bid = { + 'bidder': 'teads', + 'params': { + 'placementId': 10433394, + 'pageId': 1234 + }, + 'adUnitCode': 'adunit-code', + 'sizes': [[300, 250], [300, 600]], + 'bidId': '30b31c1838de1e', + 'bidderRequestId': '22edbae2733bf6', + 'auctionId': '1d1a030790a475', + 'creativeId': 'er2ee' + }; + + it('should return true when required params found', () => { + expect(spec.isBidRequestValid(bid)).to.equal(true); + }); + + it('should return false when pageId is not valid (letters)', () => { + let bid = Object.assign({}, bid); + delete bid.params; + bid.params = { + 'placementId': 1234, + 'pageId': 'ABCD' + }; + + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + + it('should return false when placementId is not valid (letters)', () => { + let bid = Object.assign({}, bid); + delete bid.params; + bid.params = { + 'placementId': 'FCP', + 'pageId': 1234 + }; + + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + + it('should return false when placementId < 0 or pageId < 0', () => { + let bid = Object.assign({}, bid); + delete bid.params; + bid.params = { + 'placementId': -1, + 'pageId': -1 + }; + + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + + it('should return false when required params are not passed', () => { + let bid = Object.assign({}, bid); + delete bid.params; + + bid.params = { + 'placementId': 0 + }; + + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + }); + + describe('buildRequests', () => { + let bidRequests = [ + { + 'bidder': 'teads', + 'params': { + 'placementId': 10433394, + 'pageId': 1234 + }, + 'adUnitCode': 'adunit-code', + 'sizes': [[300, 250], [300, 600]], + 'bidId': '30b31c1838de1e', + 'bidderRequestId': '22edbae2733bf6', + 'auctionId': '1d1a030790a475', + 'creativeId': 'er2ee', + 'deviceWidth': 1680 + } + ]; + + it('sends bid request to ENDPOINT via POST', () => { + let bidderRequest = { + 'bidderCode': 'teads', + 'auctionId': '1d1a030790a475', + 'bidderRequestId': '22edbae2733bf6', + 'timeout': 3000 + }; + const request = spec.buildRequests(bidRequests, bidderRequest); + + expect(request.url).to.equal(ENDPOINT); + expect(request.method).to.equal('POST'); + }); + + it('should send GDPR to endpoint', () => { + let consentString = 'JRJ8RKfDeBNsERRDCSAAZ+A=='; + let bidderRequest = { + 'bidderCode': 'teads', + 'auctionId': '1d1a030790a475', + 'bidderRequestId': '22edbae2733bf6', + 'timeout': 3000, + 'gdprConsent': { + 'consentString': consentString, + 'gdprApplies': true, + 'vendorData': { + 'hasGlobalConsent': false + } + } + }; + + const request = spec.buildRequests(bidRequests, bidderRequest); + const payload = JSON.parse(request.data); + + expect(payload.gdpr_iab).to.exist; + expect(payload.gdpr_iab.consent).to.equal(consentString); + expect(payload.gdpr_iab.status).to.equal(12); + }) + + it('should send GDPR to endpoint with 11 status', () => { + let consentString = 'JRJ8RKfDeBNsERRDCSAAZ+A=='; + let bidderRequest = { + 'bidderCode': 'teads', + 'auctionId': '1d1a030790a475', + 'bidderRequestId': '22edbae2733bf6', + 'timeout': 3000, + 'gdprConsent': { + 'consentString': consentString, + 'gdprApplies': true, + 'vendorData': { + 'hasGlobalScope': true + } + } + }; + + const request = spec.buildRequests(bidRequests, bidderRequest); + const payload = JSON.parse(request.data); + + expect(payload.gdpr_iab).to.exist; + expect(payload.gdpr_iab.consent).to.equal(consentString); + expect(payload.gdpr_iab.status).to.equal(11); + }) + + it('should send GDPR to endpoint with 22 status', () => { + let consentString = 'JRJ8RKfDeBNsERRDCSAAZ+A=='; + let bidderRequest = { + 'bidderCode': 'teads', + 'auctionId': '1d1a030790a475', + 'bidderRequestId': '22edbae2733bf6', + 'timeout': 3000, + 'gdprConsent': { + 'consentString': undefined, + 'gdprApplies': undefined, + 'vendorData': undefined + } + }; + + const request = spec.buildRequests(bidRequests, bidderRequest); + const payload = JSON.parse(request.data); + + expect(payload.gdpr_iab).to.exist; + expect(payload.gdpr_iab.consent).to.equal(''); + expect(payload.gdpr_iab.status).to.equal(22); + }) + + it('should send GDPR to endpoint with 0 status', () => { + let consentString = 'JRJ8RKfDeBNsERRDCSAAZ+A=='; + let bidderRequest = { + 'bidderCode': 'teads', + 'auctionId': '1d1a030790a475', + 'bidderRequestId': '22edbae2733bf6', + 'timeout': 3000, + 'gdprConsent': { + 'consentString': consentString, + 'gdprApplies': false, + 'vendorData': { + 'hasGlobalScope': false + } + } + }; + + const request = spec.buildRequests(bidRequests, bidderRequest); + const payload = JSON.parse(request.data); + + expect(payload.gdpr_iab).to.exist; + expect(payload.gdpr_iab.consent).to.equal(consentString); + expect(payload.gdpr_iab.status).to.equal(0); + }) + }); + + describe('interpretResponse', () => { + let bids = { + 'body': { + 'responses': [{ + 'ad': AD_SCRIPT, + 'bidderCode': 'teads', + 'cpm': 0.5, + 'currency': 'USD', + 'height': 250, + 'netRevenue': true, + 'requestId': '3ede2a3fa0db94', + 'ttl': 360, + 'width': 300, + 'creativeId': 'er2ee' + }] + } + }; + + it('should get correct bid response', () => { + let expectedResponse = [{ + 'bidderCode': 'teads', + 'cpm': 0.5, + 'width': 300, + 'height': 250, + 'currency': 'USD', + 'netRevenue': true, + 'ttl': 360, + 'ad': AD_SCRIPT, + 'requestId': '3ede2a3fa0db94', + 'creativeId': 'er2ee' + }]; + + let result = spec.interpretResponse(bids); + expect(Object.keys(result[0])).to.deep.equal(Object.keys(expectedResponse[0])); + }); + + it('handles nobid responses', () => { + let bids = { + 'body': { + 'responses': [] + } + }; + + let result = spec.interpretResponse(bids); + expect(result.length).to.equal(0); + }); + }); +}); From 17f6e35556b7d6568c0e81cf52349be70e4d6fff Mon Sep 17 00:00:00 2001 From: Valentin Souche Date: Mon, 1 Oct 2018 10:32:45 +0200 Subject: [PATCH 2/2] Remove bidder code & tests arrow functions --- modules/teadsBidAdapter.js | 1 - test/spec/modules/teadsBidAdapter_spec.js | 43 ++++++++++------------- 2 files changed, 18 insertions(+), 26 deletions(-) diff --git a/modules/teadsBidAdapter.js b/modules/teadsBidAdapter.js index 2beaa36c925..e8dbe4e1c6b 100644 --- a/modules/teadsBidAdapter.js +++ b/modules/teadsBidAdapter.js @@ -76,7 +76,6 @@ export const spec = { if (serverResponse.responses) { serverResponse.responses.forEach(function (bid) { const bidResponse = { - bidderCode: spec.code, cpm: bid.cpm, width: bid.width, height: bid.height, diff --git a/test/spec/modules/teadsBidAdapter_spec.js b/test/spec/modules/teadsBidAdapter_spec.js index 6b5e08dab08..ab34c600a53 100644 --- a/test/spec/modules/teadsBidAdapter_spec.js +++ b/test/spec/modules/teadsBidAdapter_spec.js @@ -5,16 +5,16 @@ import {newBidder} from 'src/adapters/bidderFactory'; const ENDPOINT = '//a.teads.tv/hb/bid-request'; const AD_SCRIPT = '"'; -describe('teadsBidAdapter', () => { +describe('teadsBidAdapter', function() { const adapter = newBidder(spec); - describe('inherited functions', () => { - it('exists and is a function', () => { + describe('inherited functions', function() { + it('exists and is a function', function() { expect(adapter.callBids).to.exist.and.to.be.a('function'); }); }); - describe('isBidRequestValid', () => { + describe('isBidRequestValid', function() { let bid = { 'bidder': 'teads', 'params': { @@ -29,11 +29,11 @@ describe('teadsBidAdapter', () => { 'creativeId': 'er2ee' }; - it('should return true when required params found', () => { + it('should return true when required params found', function() { expect(spec.isBidRequestValid(bid)).to.equal(true); }); - it('should return false when pageId is not valid (letters)', () => { + it('should return false when pageId is not valid (letters)', function() { let bid = Object.assign({}, bid); delete bid.params; bid.params = { @@ -44,7 +44,7 @@ describe('teadsBidAdapter', () => { expect(spec.isBidRequestValid(bid)).to.equal(false); }); - it('should return false when placementId is not valid (letters)', () => { + it('should return false when placementId is not valid (letters)', function() { let bid = Object.assign({}, bid); delete bid.params; bid.params = { @@ -55,7 +55,7 @@ describe('teadsBidAdapter', () => { expect(spec.isBidRequestValid(bid)).to.equal(false); }); - it('should return false when placementId < 0 or pageId < 0', () => { + it('should return false when placementId < 0 or pageId < 0', function() { let bid = Object.assign({}, bid); delete bid.params; bid.params = { @@ -66,7 +66,7 @@ describe('teadsBidAdapter', () => { expect(spec.isBidRequestValid(bid)).to.equal(false); }); - it('should return false when required params are not passed', () => { + it('should return false when required params are not passed', function() { let bid = Object.assign({}, bid); delete bid.params; @@ -78,7 +78,7 @@ describe('teadsBidAdapter', () => { }); }); - describe('buildRequests', () => { + describe('buildRequests', function() { let bidRequests = [ { 'bidder': 'teads', @@ -96,9 +96,8 @@ describe('teadsBidAdapter', () => { } ]; - it('sends bid request to ENDPOINT via POST', () => { + it('sends bid request to ENDPOINT via POST', function() { let bidderRequest = { - 'bidderCode': 'teads', 'auctionId': '1d1a030790a475', 'bidderRequestId': '22edbae2733bf6', 'timeout': 3000 @@ -109,10 +108,9 @@ describe('teadsBidAdapter', () => { expect(request.method).to.equal('POST'); }); - it('should send GDPR to endpoint', () => { + it('should send GDPR to endpoint', function() { let consentString = 'JRJ8RKfDeBNsERRDCSAAZ+A=='; let bidderRequest = { - 'bidderCode': 'teads', 'auctionId': '1d1a030790a475', 'bidderRequestId': '22edbae2733bf6', 'timeout': 3000, @@ -133,10 +131,9 @@ describe('teadsBidAdapter', () => { expect(payload.gdpr_iab.status).to.equal(12); }) - it('should send GDPR to endpoint with 11 status', () => { + it('should send GDPR to endpoint with 11 status', function() { let consentString = 'JRJ8RKfDeBNsERRDCSAAZ+A=='; let bidderRequest = { - 'bidderCode': 'teads', 'auctionId': '1d1a030790a475', 'bidderRequestId': '22edbae2733bf6', 'timeout': 3000, @@ -157,10 +154,9 @@ describe('teadsBidAdapter', () => { expect(payload.gdpr_iab.status).to.equal(11); }) - it('should send GDPR to endpoint with 22 status', () => { + it('should send GDPR to endpoint with 22 status', function() { let consentString = 'JRJ8RKfDeBNsERRDCSAAZ+A=='; let bidderRequest = { - 'bidderCode': 'teads', 'auctionId': '1d1a030790a475', 'bidderRequestId': '22edbae2733bf6', 'timeout': 3000, @@ -179,10 +175,9 @@ describe('teadsBidAdapter', () => { expect(payload.gdpr_iab.status).to.equal(22); }) - it('should send GDPR to endpoint with 0 status', () => { + it('should send GDPR to endpoint with 0 status', function() { let consentString = 'JRJ8RKfDeBNsERRDCSAAZ+A=='; let bidderRequest = { - 'bidderCode': 'teads', 'auctionId': '1d1a030790a475', 'bidderRequestId': '22edbae2733bf6', 'timeout': 3000, @@ -204,12 +199,11 @@ describe('teadsBidAdapter', () => { }) }); - describe('interpretResponse', () => { + describe('interpretResponse', function() { let bids = { 'body': { 'responses': [{ 'ad': AD_SCRIPT, - 'bidderCode': 'teads', 'cpm': 0.5, 'currency': 'USD', 'height': 250, @@ -222,9 +216,8 @@ describe('teadsBidAdapter', () => { } }; - it('should get correct bid response', () => { + it('should get correct bid response', function() { let expectedResponse = [{ - 'bidderCode': 'teads', 'cpm': 0.5, 'width': 300, 'height': 250, @@ -240,7 +233,7 @@ describe('teadsBidAdapter', () => { expect(Object.keys(result[0])).to.deep.equal(Object.keys(expectedResponse[0])); }); - it('handles nobid responses', () => { + it('handles nobid responses', function() { let bids = { 'body': { 'responses': []