From 28f538bf06a4c87dcead210cd277df4336fa8a48 Mon Sep 17 00:00:00 2001 From: "Hong Jen-Yee (PCMan)" Date: Tue, 18 Sep 2018 16:29:55 +0800 Subject: [PATCH] Add a new adapter for Appier bidder. --- modules/appierBidAdapter.js | 89 +++++++++++ modules/appierBidAdapter.md | 57 +++++++ test/spec/modules/appierBidAdapter_spec.js | 174 +++++++++++++++++++++ 3 files changed, 320 insertions(+) create mode 100644 modules/appierBidAdapter.js create mode 100644 modules/appierBidAdapter.md create mode 100644 test/spec/modules/appierBidAdapter_spec.js diff --git a/modules/appierBidAdapter.js b/modules/appierBidAdapter.js new file mode 100644 index 00000000000..f000bbdc89f --- /dev/null +++ b/modules/appierBidAdapter.js @@ -0,0 +1,89 @@ +import { registerBidder } from 'src/adapters/bidderFactory'; +import { BANNER } from 'src/mediaTypes'; +import { config } from 'src/config'; + +export const ADAPTER_VERSION = '1.0.0'; +const SUPPORTED_AD_TYPES = [BANNER]; + +// we have different servers for different regions / farms +export const API_SERVERS_MAP = { + 'default': 'ad2.apx.appier.net', + 'tw': 'ad2.apx.appier.net', + 'jp': 'ad-jp.apx.appier.net' +}; + +const BIDDER_API_ENDPOINT = '/v1/prebid/bid'; + +export const spec = { + code: 'appier', + supportedMediaTypes: SUPPORTED_AD_TYPES, + + /** + * 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 typeof bid.params.hzid === 'string'; + }, + + /** + * Make a server request from the list of BidRequests. + * + * @param {bidRequests[]} - an array of bids + * @return ServerRequest Info describing the request to the server. + */ + buildRequests: function (bidRequests, bidderRequest) { + if (bidRequests.length === 0) { + return []; + } + const server = this.getApiServer(); + const bidderApiUrl = `//${server}${BIDDER_API_ENDPOINT}` + const payload = { + 'bids': bidRequests, + 'refererInfo': bidderRequest.refererInfo, + 'version': ADAPTER_VERSION + }; + return [{ + method: 'POST', + url: bidderApiUrl, + data: payload, + // keep the bidder request object for later use + bidderRequest: bidderRequest + }]; + }, + + /** + * Unpack the response from the server into a list of bids. + * + * @param {serverResponse} serverResponse A successful response from the server. + * @return {Bid[]} An array of bids which were nested inside the server. + */ + interpretResponse: function (serverResponse, serverRequest) { + if (!Array.isArray(serverResponse.body)) { + return []; + } + // server response body is an array of bid results + const bidResults = serverResponse.body; + // our server directly returns the format needed by prebid.js so no more + // transformation is needed here. + return bidResults; + }, + + /** + * Get the hostname of the server we want to use. + */ + getApiServer() { + // we may use different servers for different farms (geographical regions) + // if a server is specified explicitly, use it. otherwise, use farm specific server. + let server = config.getConfig('appier.server'); + if (!server) { + const farm = config.getConfig('appier.farm'); + server = API_SERVERS_MAP[farm] || API_SERVERS_MAP['default']; + } + return server; + } +}; + +registerBidder(spec); diff --git a/modules/appierBidAdapter.md b/modules/appierBidAdapter.md new file mode 100644 index 00000000000..92fdaab1e40 --- /dev/null +++ b/modules/appierBidAdapter.md @@ -0,0 +1,57 @@ +# Overview + +``` +Module Name: Appier Bid Adapter +Module Type: Bidder Adapter +Maintainer: apn-dev@appier.com +``` + +# Description + +Connects to Appier exchange for bids. + +NOTE: +- Appier bid adapter only supports Banner at the moment. +- Multi-currency is not supported. Please make sure you have correct DFP currency settings according to your deal with Appier. + +# Sample Ad Unit Config +``` +var adUnits = [ + // Banner adUnit + { + code: 'banner-div', + mediaTypes: { + banner: { + sizes: [[300, 250], [300,600]] + } + }, + bids: [{ + bidder: 'appier', + params: { + hzid: 'WhM5WIOp' + } + }] + } +]; +``` + +# Additional Config (Optional) +Set the "farm" to use region-specific server +``` + // use the bid server in Taiwan (country code: tw) + pbjs.setConfig({ + appier: { + 'farm': 'tw' + } + }); +``` + +Explicitly override the bid server used for bidding +``` + // use the bid server specified and override the default + pbjs.setConfig({ + appier: { + 'server': '${HOST_NAME_OF_THE_SERVER}' + } + }); +``` diff --git a/test/spec/modules/appierBidAdapter_spec.js b/test/spec/modules/appierBidAdapter_spec.js new file mode 100644 index 00000000000..c7fc5744d1c --- /dev/null +++ b/test/spec/modules/appierBidAdapter_spec.js @@ -0,0 +1,174 @@ +import { expect } from 'chai'; +import { spec, API_SERVERS_MAP, ADAPTER_VERSION } from 'modules/appierBidAdapter'; +import { newBidder } from 'src/adapters/bidderFactory'; +import { config } from 'src/config'; + +describe('AppierAdapter', 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': 'appier', + 'params': { + 'hzid': 'abcd' + }, + 'adUnitCode': 'adunit-code', + 'sizes': [[300, 250], [300, 600]], + 'bidId': '30b31c1838de1e', + 'bidderRequestId': '22edbae2733bf6', + 'auctionId': '1d1a030790a475', + }; + + it('should return true when required params zoneId found', function () { + expect(spec.isBidRequestValid(bid)).to.equal(true); + }); + + it('should return false when required param zoneId is missing', function () { + let bid = Object.assign({}, bid); + bid.params = {}; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + + it('should return false when required param zoneId has wrong type', function () { + let bid = Object.assign({}, bid); + bid.params = { + 'hzid': null + }; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + }); + + describe('buildRequests', function() { + it('should return an empty list when there are no bid requests', function() { + const fakeBidRequests = []; + const fakeBidderRequest = {}; + expect(spec.buildRequests(fakeBidRequests, fakeBidderRequest)).to.be.an('array').that.is.empty; + }); + + it('should generate a POST bid request with method, url, and data fields', function() { + const bid = { + 'bidder': 'appier', + 'params': { + 'hzid': 'abcd' + }, + 'adUnitCode': 'adunit-code', + 'sizes': [[300, 250], [300, 600]], + 'bidId': '30b31c1838de1e', + 'bidderRequestId': '22edbae2733bf6', + 'auctionId': '1d1a030790a475', + }; + const fakeBidRequests = [bid]; + const fakeBidderRequest = {refererInfo: { + 'referer': 'fakeReferer', + 'reachedTop': true, + 'numIframes': 1, + 'stack': [] + }}; + + const builtRequests = spec.buildRequests(fakeBidRequests, fakeBidderRequest); + expect(builtRequests.length).to.equal(1); + expect(builtRequests[0].method).to.equal('POST'); + expect(builtRequests[0].url).match(/v1\/prebid\/bid/); + expect(builtRequests[0].data).deep.equal({ + 'bids': fakeBidRequests, + 'refererInfo': fakeBidderRequest.refererInfo, + 'version': ADAPTER_VERSION + }); + }); + }); + + describe('interpretResponse', function() { + const bid = { + 'bidder': 'appier', + 'params': { + 'hzid': 'abcd' + }, + 'adUnitCode': 'adunit-code', + 'sizes': [[300, 250], [300, 600]], + 'bidId': '30b31c1838de1e', + 'bidderRequestId': '22edbae2733bf6', + 'auctionId': '1d1a030790a475', + }; + const fakeBidRequests = [bid]; + + it('should return an empty aray to indicate no valid bids', function() { + const fakeServerResponse = {}; + + const bidResponses = spec.interpretResponse(fakeServerResponse, fakeBidRequests); + + expect(bidResponses).is.an('array').that.is.empty; + }); + + it('should generate correct response array for bidder', function() { + const fakeBidResult = { + 'requestId': '30b31c1838de1e', + 'cpm': 0.0029346001, + 'creativeId': 'Idl0P0d5S3Ca5kVWcia-wQ', + 'width': 300, + 'height': 250, + 'currency': 'USD', + 'netRevenue': true, + 'ttl': 300, + 'ad': '
fake html
', + 'appierParams': { + 'hzid': 'test_hzid' + } + }; + const fakeServerResponse = { + headers: [], + body: [fakeBidResult] + }; + + const bidResponses = spec.interpretResponse(fakeServerResponse, fakeBidRequests); + expect(bidResponses).deep.equal([fakeBidResult]); + }); + }); + + describe('getApiServer', function() { + it('should use the server specified by setConfig(appier.server)', function() { + config.setConfig({ + 'appier': {'server': 'fake_server'} + }); + + const server = spec.getApiServer(); + + expect(server).equals('fake_server'); + }); + + it('should retrieve a farm specific hostname if server is not specpfied', function() { + config.setConfig({ + 'appier': {'farm': 'tw'} + }); + + const server = spec.getApiServer(); + + expect(server).equals(API_SERVERS_MAP['tw']); + }); + + it('if farm is not recognized, use the default farm', function() { + config.setConfig({ + 'appier': {'farm': 'no_this_farm'} + }); + + const server = spec.getApiServer(); + + expect(server).equals(API_SERVERS_MAP['default']); + }); + + it('if farm is not specified, use the default farm', function() { + config.setConfig({ + 'appier': {} + }); + + const server = spec.getApiServer(); + + expect(server).equals(API_SERVERS_MAP['default']); + }); + }); +});