From ae80789d8699f26e3e7f4616e67bc4568cb2d47f Mon Sep 17 00:00:00 2001 From: averyj Date: Mon, 11 Dec 2017 12:35:04 -0500 Subject: [PATCH 1/6] initial version --- modules/inskinBidAdapter.js | 182 +++++++++++++++ modules/inskinBidAdapter.md | 44 ++++ test/spec/modules/inskinBidAdapter_spec.js | 254 +++++++++++++++++++++ 3 files changed, 480 insertions(+) create mode 100644 modules/inskinBidAdapter.js create mode 100644 modules/inskinBidAdapter.md create mode 100644 test/spec/modules/inskinBidAdapter_spec.js diff --git a/modules/inskinBidAdapter.js b/modules/inskinBidAdapter.js new file mode 100644 index 00000000000..fbede479e80 --- /dev/null +++ b/modules/inskinBidAdapter.js @@ -0,0 +1,182 @@ +import * as utils from 'src/utils'; +import { registerBidder } from 'src/adapters/bidderFactory'; + +const BIDDER_CODE = 'inskin'; + +const CONFIG = { + 'inskin': { + 'BASE_URI': 'https://mfad.inskinad.com/api/v2' + } +}; + +export const spec = { + code: BIDDER_CODE, + + /** + * 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) { + return !!(bid.params.networkId && bid.params.siteId); + }, + + /** + * 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) { + // Do we need to group by bidder? i.e. to make multiple requests for + // different endpoints. + + let ret = { + method: 'POST', + url: '', + data: '', + bidRequest: [] + }; + + if (validBidRequests.length < 1) { + return ret; + } + + let ENDPOINT_URL; + + const data = Object.assign({ + placements: [], + time: Date.now(), + user: {}, + url: utils.getTopWindowUrl(), + referrer: document.referrer, + enableBotFiltering: true, + includePricingData: true, + parallel: true + }, validBidRequests[0].params); + + validBidRequests.map(bid => { + let config = CONFIG[bid.bidder]; + ENDPOINT_URL = config.BASE_URI; + + const placement = Object.assign({ + divName: bid.bidId, + adTypes: bid.adTypes || getSize(bid.sizes) + }, bid.params); + + if (placement.networkId && placement.siteId) { + data.placements.push(placement); + } + }); + + ret.data = JSON.stringify(data); + ret.bidRequest = validBidRequests; + ret.url = ENDPOINT_URL; + + return ret; + }, + + /** + * 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, bidRequest) { + let bid; + let bids; + let bidId; + let bidObj; + let bidResponses = []; + + bids = bidRequest.bidRequest; + + serverResponse = (serverResponse || {}).body; + for (let i = 0; i < bids.length; i++) { + bid = {}; + bidObj = bids[i]; + bidId = bidObj.bidId; + + if (serverResponse) { + const decision = serverResponse.decisions && serverResponse.decisions[bidId]; + const price = decision && decision.pricing && decision.pricing.clearPrice; + + if (decision && price) { + bid.requestId = bidId; + bid.cpm = price; + bid.width = decision.width; + bid.height = decision.height; + bid.ad = retrieveAd(decision); + bid.currency = 'USD'; + bid.creativeId = decision.adId; + bid.ttl = 360; + bid.netRevenue = true; + bid.referrer = utils.getTopWindowUrl(); + + bidResponses.push(bid); + } + } + } + + return bidResponses; + }, + + getUserSyncs: function(syncOptions) { + return []; + } +}; + +const sizeMap = [ + null, + '120x90', + '120x90', + '468x60', + '728x90', + '300x250', + '160x600', + '120x600', + '300x100', + '180x150', + '336x280', + '240x400', + '234x60', + '88x31', + '120x60', + '120x240', + '125x125', + '220x250', + '250x250', + '250x90', + '0x0', + '200x90', + '300x50', + '320x50', + '320x480', + '185x185', + '620x45', + '300x125', + '800x250' +]; + +sizeMap[77] = '970x90'; +sizeMap[123] = '970x250'; +sizeMap[43] = '300x600'; + +function getSize(sizes) { + const result = []; + sizes.forEach(function(size) { + const index = sizeMap.indexOf(size[0] + 'x' + size[1]); + if (index >= 0) { + result.push(index); + } + }); + return result; +} + +function retrieveAd(decision) { + return decision.contents && decision.contents[0] && decision.contents[0].body + utils.createTrackPixelHtml(decision.impressionUrl); +} + +registerBidder(spec); diff --git a/modules/inskinBidAdapter.md b/modules/inskinBidAdapter.md new file mode 100644 index 00000000000..419de63458f --- /dev/null +++ b/modules/inskinBidAdapter.md @@ -0,0 +1,44 @@ +# Overview + +Module Name: InSkin Bid Adapter + +Module Type: Bid Adapter + +Maintainer: + +# Description + +Connects to InSkin Media for receiving bids from configured demand sources. + +# Test Parameters +```javascript + var adUnits = [ + { + code: 'test-ad-1', + sizes: [[300, 250]], + bids: [ + { + bidder: 'inskin', + params: { + networkId: '9874', + siteId: 'xxxxx' + } + } + ] + }, + { + code: 'test-ad-2', + sizes: [[300, 250]], + bids: [ + { + bidder: 'inskin', + params: { + networkId: '9874', + siteId: 'xxxxx', + zoneId: 'xxxxx' + } + } + ] + } + ]; +``` diff --git a/test/spec/modules/inskinBidAdapter_spec.js b/test/spec/modules/inskinBidAdapter_spec.js new file mode 100644 index 00000000000..6c1616e7a0b --- /dev/null +++ b/test/spec/modules/inskinBidAdapter_spec.js @@ -0,0 +1,254 @@ +import { expect } from 'chai'; +import { spec } from 'modules/serverbidBidAdapter'; + +var bidFactory = require('src/bidfactory.js'); + +const ENDPOINT = 'https://mfad.inskinad.com/api/v2'; +const SMARTSYNC_CALLBACK = 'serverbidCallBids'; + +const REQUEST = { + 'bidderCode': 'inskin', + 'requestId': 'a4713c32-3762-4798-b342-4ab810ca770d', + 'bidderRequestId': '109f2a181342a9', + 'bidRequest': [{ + 'bidder': 'inskin', + 'params': { + 'networkId': 9969, + 'siteId': 730181 + }, + 'placementCode': 'div-gpt-ad-1487778092495-0', + 'sizes': [ + [728, 90], + [970, 90] + ], + 'bidId': '2b0f82502298c9', + 'bidderRequestId': '109f2a181342a9', + 'requestId': 'a4713c32-3762-4798-b342-4ab810ca770d' + }, + { + 'bidder': 'inskin', + 'params': { + 'networkId': 9969, + 'siteId': 730181 + }, + 'placementCode': 'div-gpt-ad-1487778092495-0', + 'sizes': [ + [728, 90], + [970, 90] + ], + 'bidId': '123', + 'bidderRequestId': '109f2a181342a9', + 'requestId': 'a4713c32-3762-4798-b342-4ab810ca770d' + }], + 'start': 1487883186070, + 'auctionStart': 1487883186069, + 'timeout': 3000 +}; + +const RESPONSE = { + 'headers': null, + 'body': { + 'user': { 'key': 'ue1-2d33e91b71e74929b4aeecc23f4376f1' }, + 'decisions': { + '2b0f82502298c9': { + 'adId': 2364764, + 'creativeId': 1950991, + 'flightId': 2788300, + 'campaignId': 542982, + 'clickUrl': 'https://mfad.inskinad.com/r', + 'impressionUrl': 'https://mfad.inskinad.com/i.gif', + 'contents': [{ + 'type': 'html', + 'body': '', + 'data': { + 'height': 90, + 'width': 728, + 'imageUrl': 'https://static.adzerk.net/Advertisers/b0ab77db8a7848c8b78931aed022a5ef.gif', + 'fileName': 'b0ab77db8a7848c8b78931aed022a5ef.gif' + }, + 'template': 'image' + }], + 'height': 90, + 'width': 728, + 'events': [], + 'pricing': {'price': 0.5, 'clearPrice': 0.5, 'revenue': 0.0005, 'rateType': 2, 'eCPM': 0.5} + }, + '123': { + 'adId': 2364764, + 'creativeId': 1950991, + 'flightId': 2788300, + 'campaignId': 542982, + 'clickUrl': 'https://mfad.inskinad.com/r', + 'impressionUrl': 'https://mfad.inskinad.com/i.gif', + 'contents': [{ + 'type': 'html', + 'body': '', + 'data': { + 'height': 90, + 'width': 728, + 'imageUrl': 'https://static.adzerk.net/Advertisers/b0ab77db8a7848c8b78931aed022a5ef.gif', + 'fileName': 'b0ab77db8a7848c8b78931aed022a5ef.gif' + }, + 'template': 'image' + }], + 'height': 90, + 'width': 728, + 'events': [], + 'pricing': {'price': 0.5, 'clearPrice': 0.5, 'revenue': 0.0005, 'rateType': 2, 'eCPM': 0.5} + } + } + } +}; + +describe('Serverbid BidAdapter', () => { + let bidRequests; + let adapter = spec; + + beforeEach(() => { + bidRequests = [ + { + bidder: 'inskin', + params: { + networkId: '9874', + siteId: 'xxxxx' + }, + placementCode: 'header-bid-tag-1', + sizes: [[300, 250], [300, 600]], + bidId: '23acc48ad47af5', + requestId: '0fb4905b-9456-4152-86be-c6f6d259ba99', + bidderRequestId: '1c56ad30b9b8ca8', + transactionId: '92489f71-1bf2-49a0-adf9-000cea934729' + } + ]; + }); + + describe('bid request validation', () => { + it('should accept valid bid requests', () => { + let bid = { + bidder: 'inskin', + params: { + networkId: '9874', + siteId: 'xxxxx' + } + }; + expect(spec.isBidRequestValid(bid)).to.equal(true); + }); + + it('should accept valid bid requests with extra fields', () => { + let bid = { + bidder: 'inskin', + params: { + networkId: '9874', + siteId: 'xxxxx', + zoneId: 'xxxxx' + } + }; + expect(spec.isBidRequestValid(bid)).to.equal(true); + }); + + it('should reject bid requests without siteId', () => { + let bid = { + bidder: 'inskin', + params: { + networkId: '9874' + } + }; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + + it('should reject bid requests without networkId', () => { + let bid = { + bidder: 'inskin', + params: { + siteId: '9874' + } + }; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + }); + + describe('buildRequests validation', () => { + it('creates request data', () => { + let request = spec.buildRequests(bidRequests); + + expect(request).to.exist.and.to.be.a('object'); + }); + + it('request to inskin should contain a url', () => { + let request = spec.buildRequests(bidRequests); + + expect(request.url).to.have.string('inskin.com'); + }); + + it('requires valid bids to make request', () => { + let request = spec.buildRequests([]); + expect(request.bidRequest).to.be.empty; + }); + + it('sends bid request to ENDPOINT via POST', () => { + let request = spec.buildRequests(bidRequests); + + expect(request.method).to.have.string('POST'); + }); + }); + describe('interpretResponse validation', () => { + it('response should have valid bidderCode', () => { + let bidRequest = spec.buildRequests(REQUEST.bidRequest); + let bid = bidFactory.createBid(1, bidRequest.bidRequest[0]); + + expect(bid.bidderCode).to.equal('inskin'); + }); + + it('response should include objects for all bids', () => { + let bids = spec.interpretResponse(RESPONSE, REQUEST); + + expect(bids.length).to.equal(2); + }); + + it('registers bids', () => { + let bids = spec.interpretResponse(RESPONSE, REQUEST); + bids.forEach(b => { + expect(b).to.have.property('cpm'); + expect(b.cpm).to.be.above(0); + expect(b).to.have.property('requestId'); + expect(b).to.have.property('cpm'); + expect(b).to.have.property('width'); + expect(b).to.have.property('height'); + expect(b).to.have.property('ad'); + expect(b).to.have.property('currency', 'USD'); + expect(b).to.have.property('creativeId'); + expect(b).to.have.property('ttl', 360); + expect(b).to.have.property('netRevenue', true); + expect(b).to.have.property('referrer'); + }); + }); + + it('handles nobid responses', () => { + let EMPTY_RESP = Object.assign({}, RESPONSE, {'body': {'decisions': null}}) + let bids = spec.interpretResponse(EMPTY_RESP, REQUEST); + + expect(bids).to.be.empty; + }); + + it('handles no server response', () => { + let bids = spec.interpretResponse(null, REQUEST); + + expect(bids).to.be.empty; + }); + }); + describe('getUserSyncs', () => { + let syncOptions = {'iframeEnabled': true}; + + it('handles empty sync options', () => { + let opts = spec.getUserSyncs({}); + + expect(opts).to.be.empty; + }); + + it('should always return empty array', () => { + let opts = spec.getUserSyncs(syncOptions); + + expect(opts).to.be.empty; + }); + }); +}); From e57e8638e86dd1e0dca2692fbd77a49d388f7c4c Mon Sep 17 00:00:00 2001 From: Catalin Ciocov Date: Wed, 13 Dec 2017 16:17:22 +0000 Subject: [PATCH 2/6] Initial implementation for Inskin/AdZerk bid adapter. --- modules/inskinBidAdapter.js | 34 ++++++++++++++++++++++++++++++---- 1 file changed, 30 insertions(+), 4 deletions(-) diff --git a/modules/inskinBidAdapter.js b/modules/inskinBidAdapter.js index fbede479e80..c3fb53191ca 100644 --- a/modules/inskinBidAdapter.js +++ b/modules/inskinBidAdapter.js @@ -63,7 +63,8 @@ export const spec = { const placement = Object.assign({ divName: bid.bidId, - adTypes: bid.adTypes || getSize(bid.sizes) + adTypes: bid.adTypes || getSize(bid.sizes), + eventIds: [40, 201, 202, 203, 204, 205, 206, 207, 208, 209, 210, 211, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, 240, 260, 261, 262, 263, 264, 265, 266, 267, 268, 269, 270, 271, 272, 273, 274, 275, 276, 277, 278, 279, 280, 281, 282, 283, 284, 285, 286, 287, 288, 289, 290, 291, 292, 293, 294, 295] }, bid.params); if (placement.networkId && placement.siteId) { @@ -90,6 +91,7 @@ export const spec = { let bidId; let bidObj; let bidResponses = []; + let bidsMap = {}; bids = bidRequest.bidRequest; @@ -99,6 +101,8 @@ export const spec = { bidObj = bids[i]; bidId = bidObj.bidId; + bidsMap[bidId] = bidObj; + if (serverResponse) { const decision = serverResponse.decisions && serverResponse.decisions[bidId]; const price = decision && decision.pricing && decision.pricing.clearPrice; @@ -108,7 +112,7 @@ export const spec = { bid.cpm = price; bid.width = decision.width; bid.height = decision.height; - bid.ad = retrieveAd(decision); + bid.ad = retrieveAd(bidId, decision); bid.currency = 'USD'; bid.creativeId = decision.adId; bid.ttl = 360; @@ -120,6 +124,28 @@ export const spec = { } } + if (bidResponses.length) { + window.addEventListener('message', function(e) { + if (!e.data || e.data.from !== 'ism-bid') { + return; + } + + const decision = serverResponse.decisions && serverResponse.decisions[e.data.bidId]; + if (!decision) { + return; + } + + const id = "ism_tag_" + Math.floor((Math.random() * 10e16)); + window[id] = { + bidId: e.data.bidId, + serverResponse + }; + const script = document.createElement('script'); + script.src = 'https://cdn.inskinad.com/isfe/publishercode/' + bidsMap[e.data.bidId].params.siteId + '/default.js?autoload&id=' + id; + document.getElementsByTagName('head')[0].appendChild(script); + }); + } + return bidResponses; }, @@ -175,8 +201,8 @@ function getSize(sizes) { return result; } -function retrieveAd(decision) { - return decision.contents && decision.contents[0] && decision.contents[0].body + utils.createTrackPixelHtml(decision.impressionUrl); +function retrieveAd(bidId, decision) { + return "