diff --git a/modules/teadsBidAdapter.js b/modules/teadsBidAdapter.js new file mode 100644 index 00000000000..e8dbe4e1c6b --- /dev/null +++ b/modules/teadsBidAdapter.js @@ -0,0 +1,134 @@ +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 = { + 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..ab34c600a53 --- /dev/null +++ b/test/spec/modules/teadsBidAdapter_spec.js @@ -0,0 +1,247 @@ +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', 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() { + 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', function() { + expect(spec.isBidRequestValid(bid)).to.equal(true); + }); + + it('should return false when pageId is not valid (letters)', function() { + 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)', function() { + 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', function() { + 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', function() { + let bid = Object.assign({}, bid); + delete bid.params; + + bid.params = { + 'placementId': 0 + }; + + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + }); + + describe('buildRequests', function() { + 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', function() { + let bidderRequest = { + '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', function() { + let consentString = 'JRJ8RKfDeBNsERRDCSAAZ+A=='; + let bidderRequest = { + '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', function() { + let consentString = 'JRJ8RKfDeBNsERRDCSAAZ+A=='; + let bidderRequest = { + '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', function() { + let consentString = 'JRJ8RKfDeBNsERRDCSAAZ+A=='; + let bidderRequest = { + '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', function() { + let consentString = 'JRJ8RKfDeBNsERRDCSAAZ+A=='; + let bidderRequest = { + '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', function() { + let bids = { + 'body': { + 'responses': [{ + 'ad': AD_SCRIPT, + 'cpm': 0.5, + 'currency': 'USD', + 'height': 250, + 'netRevenue': true, + 'requestId': '3ede2a3fa0db94', + 'ttl': 360, + 'width': 300, + 'creativeId': 'er2ee' + }] + } + }; + + it('should get correct bid response', function() { + let expectedResponse = [{ + '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', function() { + let bids = { + 'body': { + 'responses': [] + } + }; + + let result = spec.interpretResponse(bids); + expect(result.length).to.equal(0); + }); + }); +});