prebid/Prebid.js

View on GitHub
modules/minutemediaBidAdapter.js

Summary

Maintainability
F
2 wks
Test Coverage
import {
  logWarn,
  logInfo,
  isArray,
  isFn,
  deepAccess,
  isEmpty,
  contains,
  timestamp,
  triggerPixel,
  isInteger,
  getBidIdParameter
} from '../src/utils.js';
import {registerBidder} from '../src/adapters/bidderFactory.js';
import {BANNER, VIDEO} from '../src/mediaTypes.js';
import {config} from '../src/config.js';

const SUPPORTED_AD_TYPES = [BANNER, VIDEO];
const BIDDER_CODE = 'minutemedia';
const ADAPTER_VERSION = '6.0.0';
const TTL = 360;
const DEFAULT_CURRENCY = 'USD';
const SELLER_ENDPOINT = 'https://hb.minutemedia-prebid.com/';
const MODES = {
  PRODUCTION: 'hb-mm-multi',
  TEST: 'hb-multi-mm-test'
}
const SUPPORTED_SYNC_METHODS = {
  IFRAME: 'iframe',
  PIXEL: 'pixel'
}

export const spec = {
  code: BIDDER_CODE,
  gvlid: 918,
  version: ADAPTER_VERSION,
  supportedMediaTypes: SUPPORTED_AD_TYPES,
  isBidRequestValid: function (bidRequest) {
    if (!bidRequest.params) {
      logWarn('no params have been set to MinuteMedia adapter');
      return false;
    }

    if (!bidRequest.params.org) {
      logWarn('org is a mandatory param for MinuteMedia adapter');
      return false;
    }

    return true;
  },
  buildRequests: function (validBidRequests, bidderRequest) {
    const combinedRequestsObject = {};

    // use data from the first bid, to create the general params for all bids
    const generalObject = validBidRequests[0];
    const testMode = generalObject.params.testMode;

    combinedRequestsObject.params = generateGeneralParams(generalObject, bidderRequest);
    combinedRequestsObject.bids = generateBidsParams(validBidRequests, bidderRequest);

    return {
      method: 'POST',
      url: getEndpoint(testMode),
      data: combinedRequestsObject
    }
  },
  interpretResponse: function ({body}) {
    const bidResponses = [];

    if (body.bids) {
      body.bids.forEach(adUnit => {
        const bidResponse = {
          requestId: adUnit.requestId,
          cpm: adUnit.cpm,
          currency: adUnit.currency || DEFAULT_CURRENCY,
          width: adUnit.width,
          height: adUnit.height,
          ttl: adUnit.ttl || TTL,
          creativeId: adUnit.requestId,
          netRevenue: adUnit.netRevenue || true,
          nurl: adUnit.nurl,
          mediaType: adUnit.mediaType,
          meta: {
            mediaType: adUnit.mediaType
          }
        };

        if (adUnit.mediaType === VIDEO) {
          bidResponse.vastXml = adUnit.vastXml;
        } else if (adUnit.mediaType === BANNER) {
          bidResponse.ad = adUnit.ad;
        }

        if (adUnit.adomain && adUnit.adomain.length) {
          bidResponse.meta.advertiserDomains = adUnit.adomain;
        }

        bidResponses.push(bidResponse);
      });
    }

    return bidResponses;
  },
  getUserSyncs: function (syncOptions, serverResponses) {
    const syncs = [];
    for (const response of serverResponses) {
      if (syncOptions.iframeEnabled && response.body.params.userSyncURL) {
        syncs.push({
          type: 'iframe',
          url: response.body.params.userSyncURL
        });
      }
      if (syncOptions.pixelEnabled && isArray(response.body.params.userSyncPixels)) {
        const pixels = response.body.params.userSyncPixels.map(pixel => {
          return {
            type: 'image',
            url: pixel
          }
        })
        syncs.push(...pixels)
      }
    }
    return syncs;
  },
  onBidWon: function (bid) {
    if (bid == null) {
      return;
    }

    logInfo('onBidWon:', bid);
    if (bid.hasOwnProperty('nurl') && bid.nurl.length > 0) {
      triggerPixel(bid.nurl);
    }
  }
};

registerBidder(spec);

/**
 * Get floor price
 * @param bid {bid}
 * @returns {Number}
 */
function getFloor(bid, mediaType, currency) {
  if (!isFn(bid.getFloor)) {
    return 0;
  }
  let floorResult = bid.getFloor({
    currency: currency,
    mediaType: mediaType,
    size: '*'
  });
  return floorResult.currency === currency && floorResult.floor ? floorResult.floor : 0;
}

/**
 * Get the the ad sizes array from the bid
 * @param bid {bid}
 * @returns {Array}
 */
function getSizesArray(bid, mediaType) {
  let sizesArray = []

  if (deepAccess(bid, `mediaTypes.${mediaType}.sizes`)) {
    sizesArray = bid.mediaTypes[mediaType].sizes;
  } else if (Array.isArray(bid.sizes) && bid.sizes.length > 0) {
    sizesArray = bid.sizes;
  }

  return sizesArray;
}

/**
 * Get schain string value
 * @param schainObject {Object}
 * @returns {string}
 */
function getSupplyChain(schainObject) {
  if (isEmpty(schainObject)) {
    return '';
  }
  let scStr = `${schainObject.ver},${schainObject.complete}`;
  schainObject.nodes.forEach((node) => {
    scStr += '!';
    scStr += `${getEncodedValIfNotEmpty(node.asi)},`;
    scStr += `${getEncodedValIfNotEmpty(node.sid)},`;
    scStr += `${node.hp ? encodeURIComponent(node.hp) : ''},`;
    scStr += `${getEncodedValIfNotEmpty(node.rid)},`;
    scStr += `${getEncodedValIfNotEmpty(node.name)},`;
    scStr += `${getEncodedValIfNotEmpty(node.domain)}`;
  });
  return scStr;
}

/**
 * Get encoded node value
 * @param val {string}
 * @returns {string}
 */
function getEncodedValIfNotEmpty(val) {
  return !isEmpty(val) ? encodeURIComponent(val) : '';
}

/**
 * Get preferred user-sync method based on publisher configuration
 * @param bidderCode {string}
 * @returns {string}
 */
function getAllowedSyncMethod(filterSettings, bidderCode) {
  const iframeConfigsToCheck = ['all', 'iframe'];
  const pixelConfigToCheck = 'image';
  if (filterSettings && iframeConfigsToCheck.some(config => isSyncMethodAllowed(filterSettings[config], bidderCode))) {
    return SUPPORTED_SYNC_METHODS.IFRAME;
  }
  if (!filterSettings || !filterSettings[pixelConfigToCheck] || isSyncMethodAllowed(filterSettings[pixelConfigToCheck], bidderCode)) {
    return SUPPORTED_SYNC_METHODS.PIXEL;
  }
}

/**
 * Check if sync rule is supported
 * @param syncRule {Object}
 * @param bidderCode {string}
 * @returns {boolean}
 */
function isSyncMethodAllowed(syncRule, bidderCode) {
  if (!syncRule) {
    return false;
  }
  const isInclude = syncRule.filter === 'include';
  const bidders = isArray(syncRule.bidders) ? syncRule.bidders : [bidderCode];
  return isInclude && contains(bidders, bidderCode);
}

/**
 * Get the seller endpoint
 * @param testMode {boolean}
 * @returns {string}
 */
function getEndpoint(testMode) {
  return testMode
    ? SELLER_ENDPOINT + MODES.TEST
    : SELLER_ENDPOINT + MODES.PRODUCTION;
}

/**
 * get device type
 * @param uad {ua}
 * @returns {string}
 */
function getDeviceType(ua) {
  if (/ipad|android 3.0|xoom|sch-i800|playbook|tablet|kindle/i
    .test(ua.toLowerCase())) {
    return '5';
  }
  if (/iphone|ipod|android|blackberry|opera|mini|windows\sce|palm|smartphone|iemobile/i
    .test(ua.toLowerCase())) {
    return '4';
  }
  if (/smart[-_\s]?tv|hbbtv|appletv|googletv|hdmi|netcast|viera|nettv|roku|\bdtv\b|sonydtv|inettvbrowser|\btv\b/i
    .test(ua.toLowerCase())) {
    return '3';
  }
  return '1';
}

function generateBidsParams(validBidRequests, bidderRequest) {
  const bidsArray = [];

  if (validBidRequests.length) {
    validBidRequests.forEach(bid => {
      bidsArray.push(generateBidParameters(bid, bidderRequest));
    });
  }

  return bidsArray;
}

/**
 * Generate bid specific parameters
 * @param {bid} bid
 * @param {bidderRequest} bidderRequest
 * @returns {Object} bid specific params object
 */
function generateBidParameters(bid, bidderRequest) {
  const {params} = bid;
  const mediaType = isBanner(bid) ? BANNER : VIDEO;
  const sizesArray = getSizesArray(bid, mediaType);
  const currency = params.currency || config.getConfig('currency.adServerCurrency') || DEFAULT_CURRENCY;

  // fix floor price in case of NAN
  if (isNaN(params.floorPrice)) {
    params.floorPrice = 0;
  }

  const bidObject = {
    mediaType,
    adUnitCode: getBidIdParameter('adUnitCode', bid),
    sizes: sizesArray,
    currency: currency,
    floorPrice: Math.max(getFloor(bid, mediaType, currency), params.floorPrice),
    bidId: getBidIdParameter('bidId', bid),
    loop: getBidIdParameter('bidderRequestsCount', bid),
    bidderRequestId: getBidIdParameter('bidderRequestId', bid),
    transactionId: bid.ortb2Imp?.ext?.tid || '',
    coppa: 0,
  };

  const pos = deepAccess(bid, `mediaTypes.${mediaType}.pos`);
  if (pos) {
    bidObject.pos = pos;
  }

  const gpid = deepAccess(bid, `ortb2Imp.ext.gpid`);
  if (gpid) {
    bidObject.gpid = gpid;
  }

  const placementId = params.placementId || deepAccess(bid, `mediaTypes.${mediaType}.name`);
  if (placementId) {
    bidObject.placementId = placementId;
  }

  const mimes = deepAccess(bid, `mediaTypes.${mediaType}.mimes`);
  if (mimes) {
    bidObject.mimes = mimes;
  }
  const api = deepAccess(bid, `mediaTypes.${mediaType}.api`);
  if (api) {
    bidObject.api = api;
  }

  const sua = deepAccess(bid, `ortb2.device.sua`);
  if (sua) {
    bidObject.sua = sua;
  }

  const coppa = deepAccess(bid, `ortb2.regs.coppa`)
  if (coppa) {
    bidObject.coppa = 1;
  }

  if (mediaType === VIDEO) {
    const playbackMethod = deepAccess(bid, `mediaTypes.video.playbackmethod`);
    let playbackMethodValue;

    // verify playbackMethod is of type integer array, or integer only.
    if (Array.isArray(playbackMethod) && isInteger(playbackMethod[0])) {
      // only the first playbackMethod in the array will be used, according to OpenRTB 2.5 recommendation
      playbackMethodValue = playbackMethod[0];
    } else if (isInteger(playbackMethod)) {
      playbackMethodValue = playbackMethod;
    }

    if (playbackMethodValue) {
      bidObject.playbackMethod = playbackMethodValue;
    }

    const placement = deepAccess(bid, `mediaTypes.video.placement`);
    if (placement) {
      bidObject.placement = placement;
    }

    const minDuration = deepAccess(bid, `mediaTypes.video.minduration`);
    if (minDuration) {
      bidObject.minDuration = minDuration;
    }

    const maxDuration = deepAccess(bid, `mediaTypes.video.maxduration`);
    if (maxDuration) {
      bidObject.maxDuration = maxDuration;
    }

    const skip = deepAccess(bid, `mediaTypes.video.skip`);
    if (skip) {
      bidObject.skip = skip;
    }

    const linearity = deepAccess(bid, `mediaTypes.video.linearity`);
    if (linearity) {
      bidObject.linearity = linearity;
    }

    const protocols = deepAccess(bid, `mediaTypes.video.protocols`);
    if (protocols) {
      bidObject.protocols = protocols;
    }

    const plcmt = deepAccess(bid, `mediaTypes.video.plcmt`);
    if (plcmt) {
      bidObject.plcmt = plcmt;
    }
  }

  return bidObject;
}

function isBanner(bid) {
  return bid.mediaTypes && bid.mediaTypes.banner;
}

/**
 * Generate params that are common between all bids
 * @param {single bid object} generalObject
 * @param {bidderRequest} bidderRequest
 * @returns {object} the common params object
 */
function generateGeneralParams(generalObject, bidderRequest) {
  const domain = window.location.hostname;
  const {syncEnabled, filterSettings} = config.getConfig('userSync') || {};
  const {bidderCode} = bidderRequest;
  const generalBidParams = generalObject.params;
  const timeout = bidderRequest.timeout;

  // these params are snake_case instead of camelCase to allow backwards compatability on the server.
  // in the future, these will be converted to camelCase to match our convention.
  const generalParams = {
    wrapper_type: 'prebidjs',
    wrapper_vendor: '$$PREBID_GLOBAL$$',
    wrapper_version: '$prebid.version$',
    adapter_version: ADAPTER_VERSION,
    auction_start: timestamp(),
    publisher_id: generalBidParams.org,
    publisher_name: domain,
    site_domain: domain,
    dnt: (navigator.doNotTrack == 'yes' || navigator.doNotTrack == '1' || navigator.msDoNotTrack == '1') ? 1 : 0,
    device_type: getDeviceType(navigator.userAgent),
    ua: navigator.userAgent,
    is_wrapper: !!generalBidParams.isWrapper,
    session_id: generalBidParams.sessionId || getBidIdParameter('bidderRequestId', generalObject),
    tmax: timeout
  }

  const userIdsParam = getBidIdParameter('userId', generalObject);
  if (userIdsParam) {
    generalParams.userIds = JSON.stringify(userIdsParam);
  }

  const ortb2Metadata = bidderRequest.ortb2 || {};
  if (ortb2Metadata.site) {
    generalParams.site_metadata = JSON.stringify(ortb2Metadata.site);
  }
  if (ortb2Metadata.user) {
    generalParams.user_metadata = JSON.stringify(ortb2Metadata.user);
  }

  if (syncEnabled) {
    const allowedSyncMethod = getAllowedSyncMethod(filterSettings, bidderCode);
    if (allowedSyncMethod) {
      generalParams.cs_method = allowedSyncMethod;
    }
  }

  if (bidderRequest.uspConsent) {
    generalParams.us_privacy = bidderRequest.uspConsent;
  }

  if (bidderRequest && bidderRequest.gdprConsent && bidderRequest.gdprConsent.gdprApplies) {
    generalParams.gdpr = bidderRequest.gdprConsent.gdprApplies;
    generalParams.gdpr_consent = bidderRequest.gdprConsent.consentString;
  }

  if (bidderRequest.gppConsent) {
    generalParams.gpp = bidderRequest.gppConsent.gppString;
    generalParams.gpp_sid = bidderRequest.gppConsent.applicableSections;
  } else if (bidderRequest.ortb2?.regs?.gpp) {
    generalParams.gpp = bidderRequest.ortb2.regs.gpp;
    generalParams.gpp_sid = bidderRequest.ortb2.regs.gpp_sid;
  }

  if (generalBidParams.ifa) {
    generalParams.ifa = generalBidParams.ifa;
  }

  if (generalObject.schain) {
    generalParams.schain = getSupplyChain(generalObject.schain);
  }

  if (bidderRequest && bidderRequest.refererInfo) {
    generalParams.referrer = deepAccess(bidderRequest, 'refererInfo.ref');
    generalParams.page_url = deepAccess(bidderRequest, 'refererInfo.page') || deepAccess(window, 'location.href');
  }

  return generalParams
}