From 695696a69262d1000213d6620a7f45218ba13be2 Mon Sep 17 00:00:00 2001 From: Nick Jacob Date: Wed, 2 Sep 2015 11:17:55 -0400 Subject: [PATCH] amazon adapter: bid on a slot-level --- src/adapters/amazon.js | 356 ++++++++++++++++++++++++++++++++++------- src/bidmanager.js | 82 ++++++---- src/prebid.js | 132 ++++++++------- src/utils.js | 100 ++++++++++-- 4 files changed, 495 insertions(+), 175 deletions(-) diff --git a/src/adapters/amazon.js b/src/adapters/amazon.js index f90330c8d0a..389e1b7a910 100644 --- a/src/adapters/amazon.js +++ b/src/adapters/amazon.js @@ -11,71 +11,307 @@ var adloader = require('../adloader'); * @constructor */ var AmazonAdapter = function AmazonAdapter() { - var _defaultBidderSettings = { - adserverTargeting: [{ - key: "amznslots", - val: function(bidResponse) { - return bidResponse.keys; - } - }] - }; - var bids; - - function _callBids(params) { - bids = params.bids || []; - adloader.loadScript('//c.amazon-adsystem.com/aax2/amzn_ads.js', function() { - _requestBids(); - }); + + // constants + var AZ_BID_CODE = 'amazon', + AZ_SHORT_SIZE_MAP = { + '3x2': '300x250', + '7x9': '728x90', + }, + AZ_DEFAULT_SIZE = '300x250', + AZ_PUB_ID_PARAM = 'aid', + AZ_SCRIPT_URL = '//c.amazon-adsystem.com/aax2/amzn_ads.js', + AZ_CREATIVE_START = ''; + + var bidSizeMap = {}, + bidRespMap = {}, + // (3)00x(2)50 + shortSizeRxp = /^(\d)\d+x(\d)\d+$/, + // 7x9 + shortenedSizeRxp = /^\dx\d$/, + // a300x250b1 + amznKeyRxp = /[a-z]([\dx]+)[a-z](\d+)/, + allKeys, + bids, + initialBid; + + + /** @public the bidder settings */ + var _defaultBidderSettings = { + + adserverTargeting: [{ + key: 'amznslots', + val: function (bidResponse) { + return bidResponse.keys; + } + }] + + }; + + function _safeTierSort(a, b) { + var aTier = (a || {}).tier, + bTier = (b || {}).tier; + return (aTier || 0) - (bTier || 0); + } + + /** + * Given a size as a string, reduce to + * the first number of each dimension + * @param {String} size 300x250 + * @return {String} shortened size - 3x2 + */ + function _shortenedSize(size) { + + if (utils.isArray(size)) { + return (size[0] + '')[0] + 'x' + (size[1] + '')[0]; + } + + if (shortSizeRxp.exec(size)) { + var shortSize = RegExp.$1 + 'x' + RegExp.$2; + shortSizeMap[shortSize] = size; + return shortSize; + } + return size; + } + + /** + * Given a shortened size string, return + * the size that it most likely corresponds to + * based on the sizes that we've shortened + * @param {String} sizeStr 3x2 + * @return {String} full length size string 300x250 + */ + function _fullSize(sizeStr) { + if (shortenedSizeRxp.test(sizeStr)) { + var size = shortSizeMap[sizeStr] || AZ_SHORT_SIZE_MAP[sizeStr]; + + if (!size) { + utils.logError('amazon: invalid size', 'ERROR', sizeStr); + } + + return size || AZ_DEFAULT_SIZE; + } + + return sizeStr; + } + + /** + * Add the bid request for all of it's possible sizes + * so if we get back a bid for that size from az, we can + * assume that it's available for that bid + * @param {Object} bid bidrequest + */ + function _addUnitToSizes(bid) { + utils._each(bid.sizes, function (size) { + + var short = _shortenedSize(size), + sizeStr = size.join('x'); + + // add it to both the short + the full length + // version of the size, so it works under different + // key options from a9 + bidSizeMap[short] = bidSizeMap[short] || []; + bidSizeMap[sizeStr] = bidSizeMap[sizeStr] || []; + + bidSizeMap[short].push(bid); + bidSizeMap[sizeStr].push(bid); + }); + } + + /** + * Parse the amazon key string into the key itself + * as well as the size the key is for + * @param {String} keyStr e.g., a3x5b1 + * @return {Object} the bid response params + */ + function _parseKey(keyStr) { + if (!amznKeyRxp.exec(keyStr)) { + utils.logError('amazon', 'ERROR', 'invalid bid key: ' + keyStr); + return; + } else { + return { + key: keyStr, + size: RegExp.$1, + tier: parseInt(RegExp.$2) + }; + } + } + + /** + * Make a bid (status 1) for the given key + * note that these can't be filtered out without + * knowing a CPM to compare against, so we'll combine + * the bids/keys into 1 for each slot + */ + function _makeSuccessBid(bidReq, size) { + + var bid = bidfactory.createBid(1), + fullSize = _fullSize(size), + dim = fullSize.split('x'); + + bid.bidderCode = AZ_BID_CODE; + bid.sizes = bidReq.sizes; + bid.size = fullSize; + bid.width = parseInt(dim[0]); + bid.height = parseInt(dim[1]); + return bid; + } + + function _generateCreative(adKey) { + return AZ_CREATIVE_START + adKey + AZ_CREATIVE_END; + } + + function _rand(rangeMax) { + return Math.floor(Math.random() * rangeMax); + } + + /** + * Given the response for a specific size, create a bid + * since these will have a normal CPM set, we can send them + * into the auction phase without filtering + * @param {Object} response + * @param {Number} response.cpm + * @param {String} response.size (short size, e.g. 7x9) + */ + function _makeBidForResponse(response) { + + // given a response, randomize the selection from the available + // sized units + var units = bidSizeMap[response.size]; + + if (utils.isEmpty(units)) { + utils.logError('amazon', 'ERROR', 'unit does not exist'); + return; + } + + var unitIdx = _rand(units.length), + // remove the unit so we don't compete + // against ourselves + unit = units.splice(unitIdx, 1)[0], + bid = _makeSuccessBid(unit, response.size); + + // log which unit we chose + utils.logMessage('[amazon]\tselecting ' + unit.placementCode + ' from: ' + units.length + ' available units, for: ' + response.key); + + bid.ad = _generateCreative(response.key); + bid.keys = allKeys; + bid.tier = response.tier; + bid.key = response.key; + bidmanager.addBidResponse(unit.placementCode, bid); + + // mark that we made a bid for this unit, + // so we can make error/unavail bids for the + // rest of the units + bidRespMap[unit.placementCode] = true; + } + + /** + * @public + * The entrypoint to the adapter. Load the amazon + * library (which will call the slots) and then request + * the targeting back + * @param {Object} params the bidding parameters + * @param {Array} params.bids the bids + */ + function _callBids(params) { + + if (utils.isEmpty(params.bids)) { + utils.logError('amazon', 'ERROR', 'no bids present in request'); + return; + } + + // the units which we want to participate + // in the amazon header bidding + initialBid = params.bids[0]; + bids = params.bids; + + utils._each(params.bids, function (bid) { + _addUnitToSizes(bid); + }); + + adloader.loadScript(AZ_SCRIPT_URL, _requestBids); } + /** + * Create error (unavailable) bids for each + * slot that requested an amazon bid. Since it's + * all in one request/response, we need to manually + * create multiple errors so we can finish bid responses if + * that's it + */ + function _createErrorBid() { + utils._each(bids, function (bidReq) { + var bid = bidfactory.createBid(2); + bid.bidderCode = 'amazon'; + bidmanager.addBidResponse(bidReq.placementCode, bid); + }); + } + /** + * The callback/response handler. + * This will create a bid response for each key + * that comes back from amazon, for each unit that matches that size + */ function _requestBids() { - if (amznads) { - - var adIds = bids.map(function(bid) { - return bid.params.aid; - }); - - amznads.getAdsCallback(adIds, function() { - var adResponse; - var placementCode = bids[0].placementCode; - var keys = amznads.getKeys(); - - if (keys.length) { - adResponse = bidfactory.createBid(1); - adResponse.bidderCode = 'amazon'; - adResponse.keys = keys; - - bidmanager.addBidResponse(placementCode, adResponse); - - } else { - // Indicate an ad was not returned - adResponse = bidfactory.createBid(2); - adResponse.bidderCode = 'amazon'; - bidmanager.addBidResponse(placementCode, adResponse); - } - }); - } - } - /* - function _defaultBidderSettings() { - return { - adserverTargeting: [ - { - key: "amznslots", - val: function (bidResponse) { - return bidResponse.keys; - } - } - ] - }; + if (!window.amznads) { + utils.logError('amazon', 'ERROR', 'amznads is not available'); + return; + } + + // get the amazon publisher id from the first bid + var aId = initialBid.params[AZ_PUB_ID_PARAM]; + + if (utils.isEmpty(aId)) { + utils.logError('amazon', 'ERROR', 'aId is not set in any of the bids: ' + aId); + return; + } + + amznads.getAdsCallback(aId, function() { + + // get all of the keys + allKeys = amznads.getKeys(); + if (utils.isEmpty(allKeys)) { + utils.logError('amazon', 'ERROR', 'empty response from amazon'); + return _createErrorBid(); + } + + var responsesBySize = {}; + utils._each(allKeys, function (key) { + var res = _parseKey(key); + if (!res) return; + responsesBySize[res.size] = responsesBySize[res.size] || []; + responsesBySize[res.size].push(res); + }); + + // iterating over the responses, get the top response for + // each bid size. Now we can make the response for that + // size. + utils._each(responsesBySize, function (responses, size) { + if (utils.isEmpty(responses)) return; + // get the best bid for the response + var top = responses.sort(_safeTierSort)[0]; + _makeBidForResponse(top); + }); + + // we need to create error bids for the rest + // of the units that didn't win an actual bid; + // otherwise we won't finish on time and timeout + // will run all the way + utils._each(bids, function (bidReq) { + if (bidRespMap[bidReq.placementCode]) return; + var bid = bidfactory.createBid(2); + bid.bidderCode = 'amazon'; + bidmanager.addBidResponse(bidReq.placementCode, bid); + }); + + }); } - */ - return { - callBids: _callBids, - defaultBidderSettings: _defaultBidderSettings - }; + return { + callBids: _callBids, + defaultBidderSettings: _defaultBidderSettings + }; }; -module.exports = AmazonAdapter; \ No newline at end of file +module.exports = AmazonAdapter; diff --git a/src/bidmanager.js b/src/bidmanager.js index a17fdd1cf38..f035ef591bb 100644 --- a/src/bidmanager.js +++ b/src/bidmanager.js @@ -62,6 +62,12 @@ exports.clearAllBidResponses = function(adUnitCode) { * This function should be called to by the BidderObject to register a new bid is in */ exports.addBidResponse = function(adUnitCode, bid) { + + // allow the user to edit the bid before it continues + // this can let publisher add proprietary/site-specific information + // (e.g., map data to a CPM) + utils.events.emit('beforeAddBidResponse', adUnitCode, bid); + var bidResponseObj = {}, statusPending = { code: 0, @@ -144,14 +150,51 @@ exports.createEmptyBidResponseObj = function() { }; }; +var _defaultBidderAdServerTargeting = [{ + key: 'hb_bidder', + val: function(bidResponse) { + return bidResponse.bidderCode; + } +}, { + key: 'hb_adid', + val: function(bidResponse) { + return bidResponse.adId; + } +}, { + key: 'hb_pb', + val: function(bidResponse) { + return bidResponse.pbMg; + } +}, { + key: 'hb_size', + val: function(bidResponse) { + return bidResponse.size; + } +}]; + + function getKeyValueTargetingPairs(bidderCode, custBidObj) { //retrive key value settings var keyValues = {}; var bidder_settings = pbjs.bidderSettings || {}; //first try to add based on bidderCode configuration if (bidderCode && custBidObj && bidder_settings && bidder_settings[bidderCode]) { - // - setKeys(keyValues, bidder_settings[bidderCode], custBidObj); + // if bidder_settings has `usesGenericKeys` settings, apply it to the bid + custBidObj.usesGenericKeys = !!(bidder_settings[bidderCode].usesGenericKeys); + + // if they didn't specify anything, but they did say they wanted to do 'generic' bidding, + // then copy it over + var adserverJsonKey = CONSTANTS.JSON_MAPPING.ADSERVER_TARGETING; + + if (!bidder_settings[bidderCode][adserverJsonKey] && custBidObj.usesGenericKeys) { + var adserverTargeting = (bidder_settings[CONSTANTS.JSON_MAPPING.BD_SETTING_STANDARD]) ? + bidder_settings[CONSTANTS.JSON_MAPPING.BD_SETTING_STANDARD][adserverJsonKey] : + _defaultBidderAdServerTargeting; + + bidder_settings[bidderCode][adserverJsonKey] = adserverTargeting; + } + + setKeys(keyValues, bidder_settings[bidderCode], custBidObj); } //next try with defaultBidderSettings else if (defaultBidderSettingsMap[bidderCode]) { @@ -159,33 +202,11 @@ function getKeyValueTargetingPairs(bidderCode, custBidObj) { } //now try with "generic" settings else if (custBidObj && bidder_settings) { - if (!bidder_settings[CONSTANTS.JSON_MAPPING.BD_SETTING_STANDARD]) { - bidder_settings[CONSTANTS.JSON_MAPPING.BD_SETTING_STANDARD] = { - adserverTargeting: [{ - key: 'hb_bidder', - val: function(bidResponse) { - return bidResponse.bidderCode; - } - }, { - key: 'hb_adid', - val: function(bidResponse) { - return bidResponse.adId; - } - }, { - key: 'hb_pb', - val: function(bidResponse) { - return bidResponse.pbMg; - } - }, { - key: 'hb_size', - val: function(bidResponse) { - return bidResponse.size; - - } - }] - }; - } - + if (!bidder_settings[CONSTANTS.JSON_MAPPING.BD_SETTING_STANDARD]) { + bidder_settings[CONSTANTS.JSON_MAPPING.BD_SETTING_STANDARD] = { + adserverTargeting: _defaultBidderAdServerTargeting + }; + } custBidObj.usesGenericKeys = true; setKeys(keyValues, bidder_settings[CONSTANTS.JSON_MAPPING.BD_SETTING_STANDARD], custBidObj); } @@ -308,7 +329,8 @@ exports.setBidderMap = function(bidderMap){ */ exports.checkIfAllBidsAreIn = function(adUnitCode) { - if (bidRequestCount !== 0 && bidRequestCount === bidResponseRecievedCount) { + + if (bidRequestCount !== 0 && bidRequestCount <= bidResponseRecievedCount) { _allBidsAvailable = true; } diff --git a/src/prebid.js b/src/prebid.js index e31166cb4a5..d33abbf5f09 100644 --- a/src/prebid.js +++ b/src/prebid.js @@ -160,36 +160,25 @@ function storeBidRequestByBidder(placementCode, sizes, bids) { } } -//use in place of hasOwnPropery - as opposed to a polyfill -function hasOwn(o, p) { - if (o.hasOwnProperty) { - return o.hasOwnProperty(p); - } else { - return (typeof o[p] !== objectType_undefined) && (o.constructor.prototype[p] !== o[p]); - } -} - -function isEmptyObject(obj) { - var name; - for (name in obj) { - return false; - } - return true; -} - function getWinningBid(bidArray) { - var winningBid = {}; + // callback before we try to get the winner; + // gives the adapter or the end-user an opportunity + // to inject their own logic + utils.events.emit('beforeWinningBid', bidArray); + + var winningBid; if (bidArray && bidArray.length !== 0) { bidArray.sort(function(a, b) { - //put the highest CPM first - return b.cpm - a.cpm; + var aCpm = (a || {}).cpm, + bCpm = (b || {}).cpm; + return (bCpm || 0.0) - (aCpm || 0.0); }); + //the first item has the highest cpm winningBid = bidArray[0]; - //TODO : if winning bid CPM === 0 - we need to indicate no targeting should be set } - return winningBid.bid; + return winningBid.bid; } @@ -198,6 +187,8 @@ function setGPTAsyncTargeting(code, slot, adUnitBids) { if (adUnitBids.bids.length !== 0) { for (var i = 0; i < adUnitBids.bids.length; i++) { var bid = adUnitBids.bids[i]; + if (!bid) continue; + //if use the generic key push into array with CPM for sorting if (bid.usesGenericKeys) { bidArrayTargeting.push({ @@ -220,8 +211,6 @@ function setGPTAsyncTargeting(code, slot, adUnitBids) { } } } - - } } else { @@ -269,30 +258,35 @@ function getBidResponsesByAdUnit(adunitCode) { * Copies bids into a bidArray response */ function buildBidResponse(bidArray) { + + // there's no array, we won't be setting anything + // from here + if (utils.isEmpty(bidArray)) return; + var bidResponseArray = []; //temp array to hold auction for bids var bidArrayTargeting = []; var bidClone = {}; - if (bidArray) { - for (var i = 0; i < bidArray.length; i++) { - var bid = bidArray[i]; - //clone by json parse. This also gets rid of unwanted function properties - bidClone = getCloneBid(bid); - - if (!bid.usesGenericKeys) { - //put unique key into targeting - pb_targetingMap[bidClone.adUnitCode] = bidClone.adserverTargeting; - } else { - //else put into auction array - bidArrayTargeting.push({ - cpm: bid.cpm, - bid: bid - }); - } - //put all bids into bidArray by default - bidResponseArray.push(bidClone); - } - } + for (var i = 0; i < bidArray.length; i++) { + var bid = bidArray[i]; + if (!bid) continue; + + //clone by json parse. This also gets rid of unwanted function properties + bidClone = getCloneBid(bid); + + if (!bid.usesGenericKeys) { + //put unique key into targeting + pb_targetingMap[bidClone.adUnitCode] = bidClone.adserverTargeting; + } else { + //else put into auction array + bidArrayTargeting.push({ + cpm: bid.cpm, + bid: bid + }); + } + //put all bids into bidArray by default + bidResponseArray.push(bidClone); + } if (bidArrayTargeting.length !== 0) { var winningBid = getWinningBid(bidArrayTargeting); @@ -378,6 +372,7 @@ pbjs.getBidResponses = function(adunitCode) { response = getBidResponsesByAdUnit(adunitCode); bidArray = []; if (response && response.bids) { + utils.events.emit('beforeBidResponse', response); bidArray = buildBidResponse(response.bids); } @@ -387,24 +382,15 @@ pbjs.getBidResponses = function(adunitCode) { } else { response = getBidResponsesByAdUnit(); - for (var adUnit in response) { - if (response.hasOwnProperty(adUnit)) { - if (response && response[adUnit] && response[adUnit].bids) { - bidArray = buildBidResponse(response[adUnit].bids); - } - - - returnObj[adUnit] = { - bids: bidArray - }; - - } - - } + utils._each(response, function (bidResponse, adUnit) { + utils.events.emit('beforeBidResponse', bidResponse); + returnObj[adUnit] = { + bids: (bidResponse && bidResponse.bids) ? buildBidResponse(bidResponse.bids) : [] + }; + }); } return returnObj; - }; /** * Returns bidResponses for the specified adUnitCode @@ -644,22 +630,30 @@ pbjs.addAdUnits = function(adUnitArr) { * @returns {String} id for callback */ pbjs.addCallback = function(eventStr, func) { - var id = null; - if (!eventStr || !func || typeof func !== objectType_function) { - utils.logError('error registering callback. Check method signature'); - return id; - } + var id = null; + if (!eventStr || !utils.isFn(func)) { + utils.logError('error registering callback. Check method signature'); + return id; + } + + id = utils.getUniqueIdentifierStr(); + bidmanager.addCallback(id, func, eventStr); + return id; +}; - id = utils.getUniqueIdentifierStr; - bidmanager.addCallback(id, func, eventStr); - return id; +/** + * Allow the user to bind to events + * NOTE: we don't let them emit events! + */ +pbjs.events = { + on: utils.events.on, + off: utils.events.off }; /** * Remove a callback event - * @param {string} cbId id of the callback to remove + * @param {Function} callback to remove * @alias module:pbjs.removeCallback - * @returns {String} id for callback */ pbjs.removeCallback = function(cbId) { //todo diff --git a/src/utils.js b/src/utils.js index 43d318d3ace..02c5d45522c 100644 --- a/src/utils.js +++ b/src/utils.js @@ -83,7 +83,7 @@ exports.parseSizesInput = function(sizeObj) { var sizeRegex = /^(\d)+x(\d)+$/i; if (sizes) { for (var curSizePos in sizes) { - if (hasOwn(sizes, curSizePos) && sizes[curSizePos].match(sizeRegex)) { + if (hasOwnProperty.call(sizes, curSizePos) && sizes[curSizePos].match(sizeRegex)) { parsedSizes.push(sizes[curSizePos]); } } @@ -329,24 +329,92 @@ exports.isEmpty = function(object) { return true; }; - /** - * Iterate object with the function - * falls back to es5 `forEach` - * @param {Array|Object} object - * @param {Function(value, key, object)} fn - */ +/** + * Iterate object with the function + * falls back to es5 `forEach` + * @param {Array|Object} object + * @param {Function(value, key, object)} fn + */ exports._each = function(object, fn) { - if (this.isEmpty(object)) return; - if (this.isFn(object.forEach)) return object.forEach(fn); + if (this.isEmpty(object)) return; + if (this.isFn(object.forEach)) return object.forEach(fn); - var k = 0, - l = object.length; + var k = 0, + l = object.length; - if (l > 0) { - for (; k < l; k++) fn(object[k], k, object); - } else { - for (k in object) { - if (hasOwnProperty.call(object, k)) fn(object[k], k, object); + if (l > 0) { + for (; k < l; k++) { + if (fn(object[k], k, object) === false) { + return; } } + } else { + for (k in object) { + if (hasOwnProperty.call(object, k)) { + if (fn(object[k], k, object) === false) { + return; + } + } + } + } +}; + +exports.events = (function (){ + + var utils = exports, + _handlers = {}, + _public = {}; + + function _dispatch(event, args) { + utils._each(_handlers[event], function (fn) { + if (!fn) return; + fn.apply(null, args); + }); + } + + _public.on = function (event, handler) { + _handlers[event] = _handlers[event] || []; + _handlers[event].push(handler); + }; + + _public.emit = function (event) { + var args = slice.call(arguments, 1); + _dispatch(event, args); }; + + _public.off = function (event, handler) { + var idx, + fns = _handlers[event]; + + if (utils.isEmpty(fns)) { + return; + } + + if ((idx = fns.indexOf(handler)) === -1) { + return; + } + + // in our dispatch function, + // we won't try to call null spaces + fns[idx] = null; + }; + + return _public; +}()); + +/** + * Map an array or object into another array + * given a function + * @param {Array|Object} object + * @param {Function(value, key, object)} callback + * @return {Array} + */ +exports._map = function (object, callback) { + if (this.isEmpty(object)) return []; + if (this.isFn(object.map)) return object.map(callback); + var output = []; + this._each(object, function (value, key) { + output.push(callback(value, key, object)); + }); + return output; +};