diff --git a/modules/programmaticaBidAdapter.js b/modules/programmaticaBidAdapter.js new file mode 100644 index 00000000000..7d52e305189 --- /dev/null +++ b/modules/programmaticaBidAdapter.js @@ -0,0 +1,153 @@ +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { BANNER, VIDEO } from '../src/mediaTypes.js'; +import { hasPurpose1Consent } from '../src/utils/gpdr.js'; +import { deepAccess, parseSizesInput, isArray } from '../src/utils.js'; + +const BIDDER_CODE = 'programmatica'; +const DEFAULT_ENDPOINT = 'asr.programmatica.com'; +const SYNC_ENDPOINT = 'sync.programmatica.com'; +const ADOMAIN = 'programmatica.com'; +const TIME_TO_LIVE = 360; + +export const spec = { + code: BIDDER_CODE, + + isBidRequestValid: function(bid) { + let valid = bid.params.siteId && bid.params.placementId; + + return !!valid; + }, + + buildRequests: function(validBidRequests, bidderRequest) { + let requests = []; + for (const bid of validBidRequests) { + let endpoint = bid.params.endpoint || DEFAULT_ENDPOINT; + + requests.push({ + method: 'GET', + url: `https://${endpoint}/get`, + data: { + site_id: bid.params.siteId, + placement_id: bid.params.placementId, + prebid: true, + }, + bidRequest: bid, + }); + } + + return requests; + }, + + interpretResponse: function(serverResponse, request) { + if (!serverResponse?.body?.content?.data) { + return []; + } + + const bidResponses = []; + const body = serverResponse.body; + + let mediaType = BANNER; + let ad, vastXml; + let width; + let height; + + let sizes = getSize(body.size); + if (isArray(sizes)) { + [width, height] = sizes; + } + + if (body.type.format != '') { + // banner + ad = body.content.data; + if (body.content.imps?.length) { + for (const imp of body.content.imps) { + ad += ``; + } + } + } else { + // video + vastXml = body.content.data; + mediaType = VIDEO; + + if (!width || !height) { + const pSize = deepAccess(request.bidRequest, 'mediaTypes.video.playerSize'); + const reqSize = getSize(pSize); + if (isArray(reqSize)) { + [width, height] = reqSize; + } + } + } + + const bidResponse = { + requestId: request.bidRequest.bidId, + cpm: body.cpm, + currency: body.currency || 'USD', + width: parseInt(width), + height: parseInt(height), + creativeId: body.id, + netRevenue: true, + ttl: TIME_TO_LIVE, + ad: ad, + mediaType: mediaType, + vastXml: vastXml, + meta: { + advertiserDomains: [ADOMAIN], + } + }; + + if ((mediaType === VIDEO && request.bidRequest.mediaTypes?.video) || (mediaType === BANNER && request.bidRequest.mediaTypes?.banner)) { + bidResponses.push(bidResponse); + } + + return bidResponses; + }, + + getUserSyncs: function(syncOptions, serverResponses, gdprConsent, uspConsent) { + const syncs = [] + + if (!hasPurpose1Consent(gdprConsent)) { + return syncs; + } + + let params = `usp=${uspConsent ?? ''}&consent=${gdprConsent?.consentString ?? ''}`; + if (typeof gdprConsent?.gdprApplies === 'boolean') { + params += `&gdpr=${Number(gdprConsent.gdprApplies)}`; + } + + if (syncOptions.iframeEnabled) { + syncs.push({ + type: 'iframe', + url: `//${SYNC_ENDPOINT}/match/sp.ifr?${params}` + }); + } + + if (syncOptions.pixelEnabled) { + syncs.push({ + type: 'image', + url: `//${SYNC_ENDPOINT}/match/sp?${params}` + }); + } + + return syncs; + }, + + onTimeout: function(timeoutData) {}, + onBidWon: function(bid) {}, + onSetTargeting: function(bid) {}, + onBidderError: function() {}, + supportedMediaTypes: [ BANNER, VIDEO ] +} + +registerBidder(spec); + +function getSize(paramSizes) { + const parsedSizes = parseSizesInput(paramSizes); + const sizes = parsedSizes.map(size => { + const [width, height] = size.split('x'); + const w = parseInt(width, 10); + const h = parseInt(height, 10); + return [w, h]; + }); + + return sizes[0] || null; +} diff --git a/modules/programmaticaBidAdapter.md b/modules/programmaticaBidAdapter.md new file mode 100644 index 00000000000..5982edf143e --- /dev/null +++ b/modules/programmaticaBidAdapter.md @@ -0,0 +1,46 @@ +# Overview + +``` +Module Name: Programmatica Bid Adapter +Module Type: Bidder Adapter +Maintainer: tech@programmatica.com +``` + +# Description +Connects to Programmatica server for bids. +Module supports banner and video mediaType. + +# Test Parameters + +``` + var adUnits = [{ + code: '/test/div', + mediaTypes: { + banner: { + sizes: [[300, 250]] + } + }, + bids: [{ + bidder: 'programmatica', + params: { + siteId: 'cga9l34ipgja79esubrg', + placementId: 'cgim20sipgj0vj1cb510' + } + }] + }, + { + code: '/test/div', + mediaTypes: { + video: { + playerSize: [[640, 360]] + } + }, + bids: [{ + bidder: 'programmatica', + params: { + siteId: 'cga9l34ipgja79esubrg', + placementId: 'cioghpcipgj8r721e9ag' + } + }] + },]; +``` diff --git a/test/spec/modules/programmaticaBidAdapter_spec.js b/test/spec/modules/programmaticaBidAdapter_spec.js new file mode 100644 index 00000000000..247d20752c3 --- /dev/null +++ b/test/spec/modules/programmaticaBidAdapter_spec.js @@ -0,0 +1,263 @@ +import { expect } from 'chai'; +import { spec } from 'modules/programmaticaBidAdapter.js'; +import { deepClone } from 'src/utils.js'; + +describe('programmaticaBidAdapterTests', function () { + let bidRequestData = { + bids: [ + { + bidId: 'testbid', + bidder: 'programmatica', + params: { + siteId: 'testsite', + placementId: 'testplacement', + }, + sizes: [[300, 250]] + } + ] + }; + let request = []; + + it('validate_pub_params', function () { + expect( + spec.isBidRequestValid({ + bidder: 'programmatica', + params: { + siteId: 'testsite', + placementId: 'testplacement', + } + }) + ).to.equal(true); + }); + + it('validate_generated_url', function () { + const request = spec.buildRequests(deepClone(bidRequestData.bids), { timeout: 1234 }); + let req_url = request[0].url; + + expect(req_url).to.equal('https://asr.programmatica.com/get'); + }); + + it('validate_response_params', function () { + let serverResponse = { + body: { + 'id': 'crid', + 'type': { + 'format': 'Image', + 'source': 'passback', + 'dspId': '', + 'dspCreativeId': '' + }, + 'content': { + 'data': 'test ad', + 'imps': null, + 'click': { + 'url': '', + 'track': null + } + }, + 'size': '300x250', + 'matching': '', + 'cpm': 10, + 'currency': 'USD' + } + }; + + const bidRequest = deepClone(bidRequestData.bids) + bidRequest[0].mediaTypes = { + banner: {} + } + + const request = spec.buildRequests(bidRequest); + let bids = spec.interpretResponse(serverResponse, request[0]); + expect(bids).to.have.lengthOf(1); + + let bid = bids[0]; + expect(bid.ad).to.equal('test ad'); + expect(bid.cpm).to.equal(10); + expect(bid.currency).to.equal('USD'); + expect(bid.width).to.equal(300); + expect(bid.height).to.equal(250); + expect(bid.creativeId).to.equal('crid'); + expect(bid.meta.advertiserDomains).to.deep.equal(['programmatica.com']); + }); + + it('validate_response_params_imps', function () { + let serverResponse = { + body: { + 'id': 'crid', + 'type': { + 'format': 'Image', + 'source': 'passback', + 'dspId': '', + 'dspCreativeId': '' + }, + 'content': { + 'data': 'test ad', + 'imps': [ + 'testImp' + ], + 'click': { + 'url': '', + 'track': null + } + }, + 'size': '300x250', + 'matching': '', + 'cpm': 10, + 'currency': 'USD' + } + }; + + const bidRequest = deepClone(bidRequestData.bids) + bidRequest[0].mediaTypes = { + banner: {} + } + + const request = spec.buildRequests(bidRequest); + let bids = spec.interpretResponse(serverResponse, request[0]); + expect(bids).to.have.lengthOf(1); + + let bid = bids[0]; + expect(bid.ad).to.equal('test ad'); + expect(bid.cpm).to.equal(10); + expect(bid.currency).to.equal('USD'); + expect(bid.width).to.equal(300); + expect(bid.height).to.equal(250); + expect(bid.creativeId).to.equal('crid'); + expect(bid.meta.advertiserDomains).to.deep.equal(['programmatica.com']); + }) + + it('validate_invalid_response', function () { + let serverResponse = { + body: {} + }; + + const bidRequest = deepClone(bidRequestData.bids) + bidRequest[0].mediaTypes = { + banner: {} + } + + const request = spec.buildRequests(bidRequest); + let bids = spec.interpretResponse(serverResponse, request[0]); + expect(bids).to.have.lengthOf(0); + }) + + it('video_bid', function () { + const bidRequest = deepClone(bidRequestData.bids); + bidRequest[0].mediaTypes = { + video: { + playerSize: [234, 765] + } + }; + + const request = spec.buildRequests(bidRequest, { timeout: 1234 }); + const vastXml = ''; + let serverResponse = { + body: { + 'id': 'cki2n3n6snkuulqutpf0', + 'type': { + 'format': '', + 'source': 'rtb', + 'dspId': '1' + }, + 'content': { + 'data': vastXml, + 'imps': [ + 'https://asr.dev.programmatica.com/track/imp' + ], + 'click': { + 'url': '', + 'track': null + } + }, + 'size': '', + 'matching': '', + 'cpm': 70, + 'currency': 'RUB' + } + }; + + let bids = spec.interpretResponse(serverResponse, request[0]); + expect(bids).to.have.lengthOf(1); + + let bid = bids[0]; + expect(bid.mediaType).to.equal('video'); + expect(bid.vastXml).to.equal(vastXml); + expect(bid.width).to.equal(234); + expect(bid.height).to.equal(765); + }); +}); + +describe('getUserSyncs', function() { + it('returns empty sync array', function() { + const syncOptions = {}; + + expect(spec.getUserSyncs(syncOptions)).to.deep.equal([]); + }); + + it('Should return array of objects with proper sync config , include CCPA', function() { + const syncData = spec.getUserSyncs({ + pixelEnabled: true, + }, {}, {}, '1---'); + expect(syncData).to.be.an('array').which.is.not.empty; + expect(syncData[0]).to.be.an('object') + expect(syncData[0].type).to.be.a('string') + expect(syncData[0].type).to.equal('image') + expect(syncData[0].url).to.be.a('string') + expect(syncData[0].url).to.equal('//sync.programmatica.com/match/sp?usp=1---&consent=') + }); + + it('Should return array of objects with proper sync config , include GDPR', function() { + const syncData = spec.getUserSyncs({ + iframeEnabled: true, + }, {}, { + gdprApplies: true, + consentString: 'COvFyGBOvFyGBAbAAAENAPCAAOAAAAAAAAAAAEEUACCKAAA.IFoEUQQgAIQwgIwQABAEAAAAOIAACAIAAAAQAIAgEAACEAAAAAgAQBAAAAAAAGBAAgAAAAAAAFAAECAAAgAAQARAEQAAAAAJAAIAAgAAAYQEAAAQmAgBC3ZAYzUw', + vendorData: { + purpose: { + consents: { + 1: true + }, + }, + } + }, ''); + expect(syncData).to.be.an('array').which.is.not.empty; + expect(syncData[0]).to.be.an('object') + expect(syncData[0].type).to.be.a('string') + expect(syncData[0].type).to.equal('iframe') + expect(syncData[0].url).to.be.a('string') + expect(syncData[0].url).to.equal('//sync.programmatica.com/match/sp.ifr?usp=&consent=COvFyGBOvFyGBAbAAAENAPCAAOAAAAAAAAAAAEEUACCKAAA.IFoEUQQgAIQwgIwQABAEAAAAOIAACAIAAAAQAIAgEAACEAAAAAgAQBAAAAAAAGBAAgAAAAAAAFAAECAAAgAAQARAEQAAAAAJAAIAAgAAAYQEAAAQmAgBC3ZAYzUw&gdpr=1') + }); + + it('Should return array of objects with proper sync config , include GDPR, no purpose', function() { + const syncData = spec.getUserSyncs({ + iframeEnabled: true, + }, {}, { + gdprApplies: true, + consentString: 'COvFyGBOvFyGBAbAAAENAPCAAOAAAAAAAAAAAEEUACCKAAA.IFoEUQQgAIQwgIwQABAEAAAAOIAACAIAAAAQAIAgEAACEAAAAAgAQBAAAAAAAGBAAgAAAAAAAFAAECAAAgAAQARAEQAAAAAJAAIAAgAAAYQEAAAQmAgBC3ZAYzUw', + vendorData: { + purpose: { + consents: { + 1: false + }, + }, + } + }, ''); + expect(syncData).is.empty; + }); + + it('Should return array of objects with proper sync config , GDPR not applies', function() { + const syncData = spec.getUserSyncs({ + iframeEnabled: true, + }, {}, { + gdprApplies: false, + consentString: 'COvFyGBOvFyGBAbAAAENAPCAAOAAAAAAAAAAAEEUACCKAAA.IFoEUQQgAIQwgIwQABAEAAAAOIAACAIAAAAQAIAgEAACEAAAAAgAQBAAAAAAAGBAAgAAAAAAAFAAECAAAgAAQARAEQAAAAAJAAIAAgAAAYQEAAAQmAgBC3ZAYzUw', + }, ''); + expect(syncData).to.be.an('array').which.is.not.empty; + expect(syncData[0]).to.be.an('object') + expect(syncData[0].type).to.be.a('string') + expect(syncData[0].type).to.equal('iframe') + expect(syncData[0].url).to.be.a('string') + expect(syncData[0].url).to.equal('//sync.programmatica.com/match/sp.ifr?usp=&consent=COvFyGBOvFyGBAbAAAENAPCAAOAAAAAAAAAAAEEUACCKAAA.IFoEUQQgAIQwgIwQABAEAAAAOIAACAIAAAAQAIAgEAACEAAAAAgAQBAAAAAAAGBAAgAAAAAAAFAAECAAAgAAQARAEQAAAAAJAAIAAgAAAYQEAAAQmAgBC3ZAYzUw&gdpr=0') + }); +})