prebid/Prebid.js

View on GitHub
modules/adpod.js

Summary

Maintainability
F
4 days
Test Coverage
/**
 * This module houses the functionality to evaluate and process adpod adunits/bids.  Specifically there are several hooked functions,
 * that either supplement the base function (ie to check something additional or unique to adpod objects) or to replace the base function
 * entirely when appropriate.
 *
 * Brief outline of each hook:
 * - `callPrebidCacheHook` - for any adpod bids, this function will temporarily hold them in a queue in order to send the bids to Prebid Cache in bulk
 * - `checkAdUnitSetupHook` - evaluates the adUnits to ensure that required fields for adpod adUnits are present.  Invalid adpod adUntis are removed from the array.
 * - `checkVideoBidSetupHook` - evaluates the adpod bid returned from an adaptor/bidder to ensure required fields are populated; also initializes duration bucket field.
 *
 * To initialize the module, there is an `initAdpodHooks()` function that should be imported and executed by a corresponding `...AdServerVideo`
 * module that designed to support adpod video type ads.  This import process allows this module to effectively act as a sub-module.
 */

import {
  deepAccess,
  generateUUID,
  groupBy,
  isArray,
  isArrayOfNums,
  isNumber,
  isPlainObject,
  logError,
  logInfo,
  logWarn
} from '../src/utils.js';
import {
  addBidToAuction,
  AUCTION_IN_PROGRESS,
  getPriceByGranularity,
  getPriceGranularity
} from '../src/auction.js';
import {checkAdUnitSetup} from '../src/prebid.js';
import {checkVideoBidSetup} from '../src/video.js';
import {getHook, module, setupBeforeHookFnOnce} from '../src/hook.js';
import {store} from '../src/videoCache.js';
import {config} from '../src/config.js';
import {ADPOD} from '../src/mediaTypes.js';
import {find, arrayFrom as from} from '../src/polyfill.js';
import {auctionManager} from '../src/auctionManager.js';
import { TARGETING_KEYS } from '../src/constants.js';

const TARGETING_KEY_PB_CAT_DUR = 'hb_pb_cat_dur';
const TARGETING_KEY_CACHE_ID = 'hb_cache_id';

let queueTimeDelay = 50;
let queueSizeLimit = 5;
let bidCacheRegistry = createBidCacheRegistry();

/**
 * Create a registry object that stores/manages bids while be held in queue for Prebid Cache.
 * @returns registry object with defined accessor functions
 */
function createBidCacheRegistry() {
  let registry = {};

  function setupRegistrySlot(auctionId) {
    registry[auctionId] = {};
    registry[auctionId].bidStorage = new Set();
    registry[auctionId].queueDispatcher = createDispatcher(queueTimeDelay);
    registry[auctionId].initialCacheKey = generateUUID();
  }

  return {
    addBid: function (bid) {
      // create parent level object based on auction ID (in case there are concurrent auctions running) to store objects for that auction
      if (!registry[bid.auctionId]) {
        setupRegistrySlot(bid.auctionId);
      }
      registry[bid.auctionId].bidStorage.add(bid);
    },
    removeBid: function (bid) {
      registry[bid.auctionId].bidStorage.delete(bid);
    },
    getBids: function (bid) {
      return registry[bid.auctionId] && registry[bid.auctionId].bidStorage.values();
    },
    getQueueDispatcher: function (bid) {
      return registry[bid.auctionId] && registry[bid.auctionId].queueDispatcher;
    },
    setupInitialCacheKey: function (bid) {
      if (!registry[bid.auctionId]) {
        registry[bid.auctionId] = {};
        registry[bid.auctionId].initialCacheKey = generateUUID();
      }
    },
    getInitialCacheKey: function (bid) {
      return registry[bid.auctionId] && registry[bid.auctionId].initialCacheKey;
    }
  }
}

/**
 * Creates a function that when called updates the bid queue and extends the running timer (when called subsequently).
 * Once the time threshold for the queue (defined by queueSizeLimit) is reached, the queue will be flushed by calling the `firePrebidCacheCall` function.
 * If there is a long enough time between calls (based on timeoutDration), the queue will automatically flush itself.
 * @param {Number} timeoutDuration number of milliseconds to pass before timer expires and current bid queue is flushed
 * @returns {Function}
 */
function createDispatcher(timeoutDuration) {
  let timeout;
  let counter = 1;

  return function (auctionInstance, bidListArr, afterBidAdded, killQueue) {
    const context = this;

    var callbackFn = function () {
      firePrebidCacheCall.call(context, auctionInstance, bidListArr, afterBidAdded);
    };

    clearTimeout(timeout);

    if (!killQueue) {
      // want to fire off the queue if either: size limit is reached or time has passed since last call to dispatcher
      if (counter === queueSizeLimit) {
        counter = 1;
        callbackFn();
      } else {
        counter++;
        timeout = setTimeout(callbackFn, timeoutDuration);
      }
    } else {
      counter = 1;
    }
  };
}

function getPricePartForAdpodKey(bid) {
  let pricePart
  let prioritizeDeals = config.getConfig('adpod.prioritizeDeals');
  if (prioritizeDeals && deepAccess(bid, 'video.dealTier')) {
    const adpodDealPrefix = config.getConfig(`adpod.dealTier.${bid.bidderCode}.prefix`);
    pricePart = (adpodDealPrefix) ? adpodDealPrefix + deepAccess(bid, 'video.dealTier') : deepAccess(bid, 'video.dealTier');
  } else {
    const granularity = getPriceGranularity(bid);
    pricePart = getPriceByGranularity(granularity)(bid);
  }
  return pricePart
}

/**
 * This function reads certain fields from the bid to generate a specific key used for caching the bid in Prebid Cache
 * @param {Object} bid bid object to update
 * @param {Boolean} brandCategoryExclusion value read from setConfig; influences whether category is required or not
 */
function attachPriceIndustryDurationKeyToBid(bid, brandCategoryExclusion) {
  let initialCacheKey = bidCacheRegistry.getInitialCacheKey(bid);
  let duration = deepAccess(bid, 'video.durationBucket');
  const pricePart = getPricePartForAdpodKey(bid);
  let pcd;

  if (brandCategoryExclusion) {
    let category = deepAccess(bid, 'meta.adServerCatId');
    pcd = `${pricePart}_${category}_${duration}s`;
  } else {
    pcd = `${pricePart}_${duration}s`;
  }

  if (!bid.adserverTargeting) {
    bid.adserverTargeting = {};
  }
  bid.adserverTargeting[TARGETING_KEY_PB_CAT_DUR] = pcd;
  bid.adserverTargeting[TARGETING_KEY_CACHE_ID] = initialCacheKey;
  bid.videoCacheKey = initialCacheKey;
  bid.customCacheKey = `${pcd}_${initialCacheKey}`;
}

/**
 * Updates the running queue for the associated auction.
 * Does a check to ensure the auction is still running; if it's not - the previously running queue is killed.
 * @param {*} auctionInstance running context of the auction
 * @param {Object} bidResponse bid object being added to queue
 * @param {Function} afterBidAdded callback function used when Prebid Cache responds
 */
function updateBidQueue(auctionInstance, bidResponse, afterBidAdded) {
  let bidListIter = bidCacheRegistry.getBids(bidResponse);

  if (bidListIter) {
    let bidListArr = from(bidListIter);
    let callDispatcher = bidCacheRegistry.getQueueDispatcher(bidResponse);
    let killQueue = !!(auctionInstance.getAuctionStatus() !== AUCTION_IN_PROGRESS);
    callDispatcher(auctionInstance, bidListArr, afterBidAdded, killQueue);
  } else {
    logWarn('Attempted to cache a bid from an unknown auction. Bid:', bidResponse);
  }
}

/**
 * Small helper function to remove bids from internal storage; normally b/c they're about to sent to Prebid Cache for processing.
 * @param {Array[Object]} bidResponses list of bids to remove
 */
function removeBidsFromStorage(bidResponses) {
  for (let i = 0; i < bidResponses.length; i++) {
    bidCacheRegistry.removeBid(bidResponses[i]);
  }
}

/**
 * This function will send a list of bids to Prebid Cache.  It also removes the same bids from the internal bidCacheRegistry
 * to maintain which bids are in queue.
 * If the bids are successfully cached, they will be added to the respective auction.
 * @param {*} auctionInstance running context of the auction
 * @param {Array[Object]} bidList list of bid objects that need to be sent to Prebid Cache
 * @param {Function} afterBidAdded callback function used when Prebid Cache responds
 */
function firePrebidCacheCall(auctionInstance, bidList, afterBidAdded) {
  // remove entries now so other incoming bids won't accidentally have a stale version of the list while PBC is processing the current submitted list
  removeBidsFromStorage(bidList);

  store(bidList, function (error, cacheIds) {
    if (error) {
      logWarn(`Failed to save to the video cache: ${error}. Video bid(s) must be discarded.`);
    } else {
      for (let i = 0; i < cacheIds.length; i++) {
        // when uuid in response is empty string then the key already existed, so this bid wasn't cached
        if (cacheIds[i].uuid !== '') {
          addBidToAuction(auctionInstance, bidList[i]);
        } else {
          logInfo(`Detected a bid was not cached because the custom key was already registered.  Attempted to use key: ${bidList[i].customCacheKey}. Bid was: `, bidList[i]);
        }
        afterBidAdded();
      }
    }
  });
}

/**
 * This is the main hook function to handle adpod bids; maintains the logic to temporarily hold bids in a queue in order to send bulk requests to Prebid Cache.
 * @param {Function} fn reference to original function (used by hook logic)
 * @param {*} auctionInstance running context of the auction
 * @param {Object} bidResponse incoming bid; if adpod, will be processed through hook function.  If not adpod, returns to original function.
 * @param {Function} afterBidAdded callback function used when Prebid Cache responds
 * @param {Object} videoConfig mediaTypes.video from the bid's adUnit
 */
export function callPrebidCacheHook(fn, auctionInstance, bidResponse, afterBidAdded, videoConfig) {
  if (videoConfig && videoConfig.context === ADPOD) {
    let brandCategoryExclusion = config.getConfig('adpod.brandCategoryExclusion');
    let adServerCatId = deepAccess(bidResponse, 'meta.adServerCatId');
    if (!adServerCatId && brandCategoryExclusion) {
      logWarn('Detected a bid without meta.adServerCatId while setConfig({adpod.brandCategoryExclusion}) was enabled.  This bid has been rejected:', bidResponse);
      afterBidAdded();
    } else {
      if (config.getConfig('adpod.deferCaching') === false) {
        bidCacheRegistry.addBid(bidResponse);
        attachPriceIndustryDurationKeyToBid(bidResponse, brandCategoryExclusion);

        updateBidQueue(auctionInstance, bidResponse, afterBidAdded);
      } else {
        // generate targeting keys for bid
        bidCacheRegistry.setupInitialCacheKey(bidResponse);
        attachPriceIndustryDurationKeyToBid(bidResponse, brandCategoryExclusion);

        // add bid to auction
        addBidToAuction(auctionInstance, bidResponse);
        afterBidAdded();
      }
    }
  } else {
    fn.call(this, auctionInstance, bidResponse, afterBidAdded, videoConfig);
  }
}

/**
 * This hook function will review the adUnit setup and verify certain required values are present in any adpod adUnits.
 * If the fields are missing or incorrectly setup, the adUnit is removed from the list.
 * @param {Function} fn reference to original function (used by hook logic)
 * @param {Array[Object]} adUnits list of adUnits to be evaluated
 * @returns {Array[Object]} list of adUnits that passed the check
 */
export function checkAdUnitSetupHook(fn, adUnits) {
  let goodAdUnits = adUnits.filter(adUnit => {
    let mediaTypes = deepAccess(adUnit, 'mediaTypes');
    let videoConfig = deepAccess(mediaTypes, 'video');
    if (videoConfig && videoConfig.context === ADPOD) {
      // run check to see if other mediaTypes are defined (ie multi-format); reject adUnit if so
      if (Object.keys(mediaTypes).length > 1) {
        logWarn(`Detected more than one mediaType in adUnitCode: ${adUnit.code} while attempting to define an 'adpod' video adUnit.  'adpod' adUnits cannot be mixed with other mediaTypes.  This adUnit will be removed from the auction.`);
        return false;
      }

      let errMsg = `Detected missing or incorrectly setup fields for an adpod adUnit.  Please review the following fields of adUnitCode: ${adUnit.code}.  This adUnit will be removed from the auction.`;

      let playerSize = !!(
        (
          videoConfig.playerSize && (
            isArrayOfNums(videoConfig.playerSize, 2) || (
              isArray(videoConfig.playerSize) && videoConfig.playerSize.every(sz => isArrayOfNums(sz, 2))
            )
          )
        ) || (videoConfig.sizeConfig)
      );
      let adPodDurationSec = !!(videoConfig.adPodDurationSec && isNumber(videoConfig.adPodDurationSec) && videoConfig.adPodDurationSec > 0);
      let durationRangeSec = !!(videoConfig.durationRangeSec && isArrayOfNums(videoConfig.durationRangeSec) && videoConfig.durationRangeSec.every(range => range > 0));

      if (!playerSize || !adPodDurationSec || !durationRangeSec) {
        errMsg += (!playerSize) ? '\nmediaTypes.video.playerSize' : '';
        errMsg += (!adPodDurationSec) ? '\nmediaTypes.video.adPodDurationSec' : '';
        errMsg += (!durationRangeSec) ? '\nmediaTypes.video.durationRangeSec' : '';
        logWarn(errMsg);
        return false;
      }
    }
    return true;
  });
  adUnits = goodAdUnits;
  fn.call(this, adUnits);
}

/**
 * This check evaluates the incoming bid's `video.durationSeconds` field and tests it against specific logic depending on adUnit config.  Summary of logic below:
 * when adUnit.mediaTypes.video.requireExactDuration is true
 *  - only bids that exactly match those listed values are accepted (don't round at all).
 *  - populate the `bid.video.durationBucket` field with the matching duration value
 * when adUnit.mediaTypes.video.requireExactDuration is false
 *  - round the duration to the next highest specified duration value based on adunit.  If the duration is above a range within a set buffer, that bid falls down into that bucket.
 *      (eg if range was [5, 15, 30] -> 2s is rounded to 5s; 17s is rounded back to 15s; 18s is rounded up to 30s)
 *  - if the bid is above the range of the listed durations (and outside the buffer), reject the bid
 *  - set the rounded duration value in the `bid.video.durationBucket` field for accepted bids
 * @param {Object} videoMediaType 'mediaTypes.video' associated to bidResponse
 * @param {Object} bidResponse incoming bidResponse being evaluated by bidderFactory
 * @returns {boolean} return false if bid duration is deemed invalid as per adUnit configuration; return true if fine
 */
function checkBidDuration(videoMediaType, bidResponse) {
  const buffer = 2;
  let bidDuration = deepAccess(bidResponse, 'video.durationSeconds');
  let adUnitRanges = videoMediaType.durationRangeSec;
  adUnitRanges.sort((a, b) => a - b); // ensure the ranges are sorted in numeric order

  if (!videoMediaType.requireExactDuration) {
    let max = Math.max(...adUnitRanges);
    if (bidDuration <= (max + buffer)) {
      let nextHighestRange = find(adUnitRanges, range => (range + buffer) >= bidDuration);
      bidResponse.video.durationBucket = nextHighestRange;
    } else {
      logWarn(`Detected a bid with a duration value outside the accepted ranges specified in adUnit.mediaTypes.video.durationRangeSec.  Rejecting bid: `, bidResponse);
      return false;
    }
  } else {
    if (find(adUnitRanges, range => range === bidDuration)) {
      bidResponse.video.durationBucket = bidDuration;
    } else {
      logWarn(`Detected a bid with a duration value not part of the list of accepted ranges specified in adUnit.mediaTypes.video.durationRangeSec.  Exact match durations must be used for this adUnit. Rejecting bid: `, bidResponse);
      return false;
    }
  }
  return true;
}

/**
 * This hooked function evaluates an adpod bid and determines if the required fields are present.
 * If it's found to not be an adpod bid, it will return to original function via hook logic
 * @param {Function} fn reference to original function (used by hook logic)
 * @param {Object} bid incoming bid object
 * @param {Object} adUnit adUnit object of associated bid
 * @param {Object} videoMediaType copy of the `bidRequest.mediaTypes.video` object; used in original function
 * @param {String} context value of the `bidRequest.mediaTypes.video.context` field; used in original function
 * @returns {boolean} this return is only used for adpod bids
 */
export function checkVideoBidSetupHook(fn, bid, adUnit, videoMediaType, context) {
  if (context === ADPOD) {
    let result = true;
    let brandCategoryExclusion = config.getConfig('adpod.brandCategoryExclusion');
    if (brandCategoryExclusion && !deepAccess(bid, 'meta.primaryCatId')) {
      result = false;
    }

    if (deepAccess(bid, 'video')) {
      if (!deepAccess(bid, 'video.context') || bid.video.context !== ADPOD) {
        result = false;
      }

      if (!deepAccess(bid, 'video.durationSeconds') || bid.video.durationSeconds <= 0) {
        result = false;
      } else {
        let isBidGood = checkBidDuration(videoMediaType, bid);
        if (!isBidGood) result = false;
      }
    }

    if (!config.getConfig('cache.url') && bid.vastXml && !bid.vastUrl) {
      logError(`
        This bid contains only vastXml and will not work when a prebid cache url is not specified.
        Try enabling prebid cache with pbjs.setConfig({ cache: {url: "..."} });
      `);
      result = false;
    };

    fn.bail(result);
  } else {
    fn.call(this, bid, adUnit, videoMediaType, context);
  }
}

/**
 * This function reads the (optional) settings for the adpod as set from the setConfig()
 * @param {Object} config contains the config settings for adpod module
 */
export function adpodSetConfig(config) {
  if (config.bidQueueTimeDelay !== undefined) {
    if (typeof config.bidQueueTimeDelay === 'number' && config.bidQueueTimeDelay > 0) {
      queueTimeDelay = config.bidQueueTimeDelay;
    } else {
      logWarn(`Detected invalid value for adpod.bidQueueTimeDelay in setConfig; must be a positive number.  Using default: ${queueTimeDelay}`)
    }
  }

  if (config.bidQueueSizeLimit !== undefined) {
    if (typeof config.bidQueueSizeLimit === 'number' && config.bidQueueSizeLimit > 0) {
      queueSizeLimit = config.bidQueueSizeLimit;
    } else {
      logWarn(`Detected invalid value for adpod.bidQueueSizeLimit in setConfig; must be a positive number.  Using default: ${queueSizeLimit}`)
    }
  }
}
config.getConfig('adpod', config => adpodSetConfig(config.adpod));

/**
 * This function initializes the adpod module's hooks.  This is called by the corresponding adserver video module.
 */
function initAdpodHooks() {
  setupBeforeHookFnOnce(getHook('callPrebidCache'), callPrebidCacheHook);
  setupBeforeHookFnOnce(checkAdUnitSetup, checkAdUnitSetupHook);
  setupBeforeHookFnOnce(checkVideoBidSetup, checkVideoBidSetupHook);
}

initAdpodHooks()

/**
 *
 * @param {Array[Object]} bids list of 'winning' bids that need to be cached
 * @param {Function} callback send the cached bids (or error) back to adserverVideoModule for further processing
 }}
 */
export function callPrebidCacheAfterAuction(bids, callback) {
  // will call PBC here and execute cb param to initialize player code
  store(bids, function (error, cacheIds) {
    if (error) {
      callback(error, null);
    } else {
      let successfulCachedBids = [];
      for (let i = 0; i < cacheIds.length; i++) {
        if (cacheIds[i] !== '') {
          successfulCachedBids.push(bids[i]);
        }
      }
      callback(null, successfulCachedBids);
    }
  })
}

/**
 * Compare function to be used in sorting long-form bids. This will compare bids on price per second.
 * @param {Object} bid
 */
export function sortByPricePerSecond(a, b) {
  if (a.adserverTargeting[TARGETING_KEYS.PRICE_BUCKET] / a.video.durationBucket < b.adserverTargeting[TARGETING_KEYS.PRICE_BUCKET] / b.video.durationBucket) {
    return 1;
  }
  if (a.adserverTargeting[TARGETING_KEYS.PRICE_BUCKET] / a.video.durationBucket > b.adserverTargeting[TARGETING_KEYS.PRICE_BUCKET] / b.video.durationBucket) {
    return -1;
  }
  return 0;
}

/**
 * This function returns targeting keyvalue pairs for long-form adserver modules. Freewheel and GAM are currently supporting Prebid long-form
 * @param {Object} options - Options for targeting.
 * @param {Array<string>} options.codes - Array of ad unit codes.
 * @param {function} options.callback - Callback function to handle the targeting key-value pairs.
 * @returns {Object} Targeting key-value pairs for ad unit codes.
 */
export function getTargeting({ codes, callback } = {}) {
  if (!callback) {
    logError('No callback function was defined in the getTargeting call.  Aborting getTargeting().');
    return;
  }
  codes = codes || [];
  const adPodAdUnits = getAdPodAdUnits(codes);
  const bidsReceived = auctionManager.getBidsReceived();
  const competiveExclusionEnabled = config.getConfig('adpod.brandCategoryExclusion');
  const deferCachingSetting = config.getConfig('adpod.deferCaching');
  const deferCachingEnabled = (typeof deferCachingSetting === 'boolean') ? deferCachingSetting : true;

  let bids = getBidsForAdpod(bidsReceived, adPodAdUnits);
  bids = (competiveExclusionEnabled || deferCachingEnabled) ? getExclusiveBids(bids) : bids;

  let prioritizeDeals = config.getConfig('adpod.prioritizeDeals');
  if (prioritizeDeals) {
    let [otherBids, highPriorityDealBids] = bids.reduce((partitions, bid) => {
      let bidDealTier = deepAccess(bid, 'video.dealTier');
      let minDealTier = config.getConfig(`adpod.dealTier.${bid.bidderCode}.minDealTier`);
      if (minDealTier && bidDealTier) {
        if (bidDealTier >= minDealTier) {
          partitions[1].push(bid)
        } else {
          partitions[0].push(bid)
        }
      } else if (bidDealTier) {
        partitions[1].push(bid)
      } else {
        partitions[0].push(bid);
      }
      return partitions;
    }, [[], []]);
    highPriorityDealBids.sort(sortByPricePerSecond);
    otherBids.sort(sortByPricePerSecond);
    bids = highPriorityDealBids.concat(otherBids);
  } else {
    bids.sort(sortByPricePerSecond);
  }

  let targeting = {};
  if (deferCachingEnabled === false) {
    adPodAdUnits.forEach((adUnit) => {
      let adPodTargeting = [];
      let adPodDurationSeconds = deepAccess(adUnit, 'mediaTypes.video.adPodDurationSec');

      bids
        .filter((bid) => bid.adUnitCode === adUnit.code)
        .forEach((bid, index, arr) => {
          if (bid.video.durationBucket <= adPodDurationSeconds) {
            adPodTargeting.push({
              [TARGETING_KEY_PB_CAT_DUR]: bid.adserverTargeting[TARGETING_KEY_PB_CAT_DUR]
            });
            adPodDurationSeconds -= bid.video.durationBucket;
          }
          if (index === arr.length - 1 && adPodTargeting.length > 0) {
            adPodTargeting.push({
              [TARGETING_KEY_CACHE_ID]: bid.adserverTargeting[TARGETING_KEY_CACHE_ID]
            });
          }
        });
      targeting[adUnit.code] = adPodTargeting;
    });

    callback(null, targeting);
  } else {
    let bidsToCache = [];
    adPodAdUnits.forEach((adUnit) => {
      let adPodDurationSeconds = deepAccess(adUnit, 'mediaTypes.video.adPodDurationSec');

      bids
        .filter((bid) => bid.adUnitCode === adUnit.code)
        .forEach((bid) => {
          if (bid.video.durationBucket <= adPodDurationSeconds) {
            bidsToCache.push(bid);
            adPodDurationSeconds -= bid.video.durationBucket;
          }
        });
    });

    callPrebidCacheAfterAuction(bidsToCache, function (error, bidsSuccessfullyCached) {
      if (error) {
        callback(error, null);
      } else {
        let groupedBids = groupBy(bidsSuccessfullyCached, 'adUnitCode');
        Object.keys(groupedBids).forEach((adUnitCode) => {
          let adPodTargeting = [];

          groupedBids[adUnitCode].forEach((bid, index, arr) => {
            adPodTargeting.push({
              [TARGETING_KEY_PB_CAT_DUR]: bid.adserverTargeting[TARGETING_KEY_PB_CAT_DUR]
            });

            if (index === arr.length - 1 && adPodTargeting.length > 0) {
              adPodTargeting.push({
                [TARGETING_KEY_CACHE_ID]: bid.adserverTargeting[TARGETING_KEY_CACHE_ID]
              });
            }
          });
          targeting[adUnitCode] = adPodTargeting;
        });

        callback(null, targeting);
      }
    });
  }
  return targeting;
}

/**
 * This function returns the adunit of mediaType adpod
 * @param {Array} codes adUnitCodes
 * @returns {Array[Object]} adunits of mediaType adpod
 */
function getAdPodAdUnits(codes) {
  return auctionManager.getAdUnits()
    .filter((adUnit) => deepAccess(adUnit, 'mediaTypes.video.context') === ADPOD)
    .filter((adUnit) => (codes.length > 0) ? codes.indexOf(adUnit.code) != -1 : true);
}

/**
 * This function will create compare function to sort on object property
 * @param {string} property
 * @returns {function} compare function to be used in sorting
 */
function compareOn(property) {
  return function compare(a, b) {
    if (a[property] < b[property]) {
      return 1;
    }
    if (a[property] > b[property]) {
      return -1;
    }
    return 0;
  }
}

/**
 * This function removes bids of same category. It will be used when competitive exclusion is enabled.
 * @param {Array[Object]} bidsReceived
 * @returns {Array[Object]} unique category bids
 */
function getExclusiveBids(bidsReceived) {
  let bids = bidsReceived
    .map((bid) => Object.assign({}, bid, { [TARGETING_KEY_PB_CAT_DUR]: bid.adserverTargeting[TARGETING_KEY_PB_CAT_DUR] }));
  bids = groupBy(bids, TARGETING_KEY_PB_CAT_DUR);
  let filteredBids = [];
  Object.keys(bids).forEach((targetingKey) => {
    bids[targetingKey].sort(compareOn('responseTimestamp'));
    filteredBids.push(bids[targetingKey][0]);
  });
  return filteredBids;
}

/**
 * This function returns bids for adpod adunits
 * @param {Array[Object]} bidsReceived
 * @param {Array[Object]} adPodAdUnits
 * @returns {Array[Object]} bids of mediaType adpod
 */
function getBidsForAdpod(bidsReceived, adPodAdUnits) {
  let adUnitCodes = adPodAdUnits.map((adUnit) => adUnit.code);
  return bidsReceived
    .filter((bid) => adUnitCodes.indexOf(bid.adUnitCode) != -1 && (bid.video && bid.video.context === ADPOD))
}

const sharedMethods = {
  TARGETING_KEY_PB_CAT_DUR: TARGETING_KEY_PB_CAT_DUR,
  TARGETING_KEY_CACHE_ID: TARGETING_KEY_CACHE_ID,
  'getTargeting': getTargeting
}
Object.freeze(sharedMethods);

module('adpod', function shareAdpodUtilities(...args) {
  if (!isPlainObject(args[0])) {
    logError('Adpod module needs plain object to share methods with submodule');
    return;
  }
  function addMethods(object, func) {
    for (let name in func) {
      object[name] = func[name];
    }
  }
  addMethods(args[0], sharedMethods);
});