prebid/Prebid.js

View on GitHub
modules/yahoosspBidAdapter.js

Summary

Maintainability
F
1 wk
Test Coverage
import { registerBidder } from '../src/adapters/bidderFactory.js';
import { BANNER, VIDEO } from '../src/mediaTypes.js';
import { deepAccess, isFn, isStr, isNumber, isArray, isEmpty, isPlainObject, generateUUID, logInfo, logWarn } from '../src/utils.js';
import { config } from '../src/config.js';
import { Renderer } from '../src/Renderer.js';
import {hasPurpose1Consent} from '../src/utils/gpdr.js';

const INTEGRATION_METHOD = 'prebid.js';
const BIDDER_CODE = 'yahooAds';
const BIDDER_ALIASES = ['yahoossp', 'yahooAdvertising']
const GVLID = 25;
const ADAPTER_VERSION = '1.1.0';
const PREBID_VERSION = '$prebid.version$';
const DEFAULT_BID_TTL = 300;
const TEST_MODE_DCN = '8a969516017a7a396ec539d97f540011';
const TEST_MODE_PUBID_DCN = '1234567';
const TEST_MODE_BANNER_POS = '8a969978017a7aaabab4ab0bc01a0009';
const TEST_MODE_VIDEO_POS = '8a96958a017a7a57ac375d50c0c700cc';
const DEFAULT_RENDERER_TIMEOUT = 700;
const DEFAULT_CURRENCY = 'USD';
const SSP_ENDPOINT_DCN_POS = 'https://c2shb.pubgw.yahoo.com/bidRequest';
const SSP_ENDPOINT_PUBID = 'https://c2shb.pubgw.yahoo.com/admax/bid/partners/PBJS';
const SUPPORTED_USER_ID_SOURCES = [
  'admixer.net',
  'adserver.org',
  'adtelligent.com',
  'akamai.com',
  'amxdt.net',
  'audigent.com',
  'britepool.com',
  'criteo.com',
  'crwdcntrl.net',
  'deepintent.com',
  'epsilon.com',
  'hcn.health',
  'id5-sync.com',
  'idx.lat',
  'intentiq.com',
  'intimatemerger.com',
  'liveintent.com',
  'liveramp.com',
  'mediawallahscript.com',
  'merkleinc.com',
  'netid.de',
  'neustar.biz',
  'nextroll.com',
  'novatiq.com',
  'parrable.com',
  'pubcid.org',
  'quantcast.com',
  'tapad.com',
  'uidapi.com',
  'yahoo.com',
  'zeotap.com'
];

/* Utility functions */

function getConfigValue(bid, key) {
  const bidderCode = bid.bidder || bid.bidderCode;
  return config.getConfig(`${bidderCode}.${key}`);
}

function getSize(size) {
  return {
    w: parseInt(size[0]),
    h: parseInt(size[1])
  }
}

function transformSizes(sizes) {
  if (isArray(sizes) && sizes.length === 2 && !isArray(sizes[0])) {
    return [ getSize(sizes) ];
  }
  return sizes.map(getSize);
}

function extractUserSyncUrls(syncOptions, pixels) {
  let itemsRegExp = /(img|iframe)[\s\S]*?src\s*=\s*("|')(.*?)\2/gi;
  let tagNameRegExp = /\w*(?=\s)/;
  let srcRegExp = /src=("|')(.*?)\1/;
  let userSyncObjects = [];

  if (pixels) {
    let matchedItems = pixels.match(itemsRegExp);
    if (matchedItems) {
      matchedItems.forEach(item => {
        let tagName = item.match(tagNameRegExp)[0];
        let url = item.match(srcRegExp)[2];

        if (tagName && url) {
          let tagType = tagName.toLowerCase() === 'img' ? 'image' : 'iframe';
          if ((!syncOptions.iframeEnabled && tagType === 'iframe') ||
                (!syncOptions.pixelEnabled && tagType === 'image')) {
            return;
          }
          userSyncObjects.push({
            type: tagType,
            url: url
          });
        }
      });
    }
  }

  return userSyncObjects;
}

/**
 * @param {string} url
 * @param {object} consentData
 * @param {object} consentData.gpp
 * @param {string} consentData.gpp.gppConsent
 * @param {Array} consentData.gpp.applicableSections
 * @param {object} consentData.gdpr
 * @param {object} consentData.gdpr.consentString
 * @param {object} consentData.gdpr.gdprApplies
 * @param {string} consentData.uspConsent
 */
function updateConsentQueryParams(url, consentData) {
  const parameterMap = {
    'gdpr_consent': consentData.gdpr ? consentData.gdpr.consentString : '',
    'gdpr': consentData.gdpr && consentData.gdpr.gdprApplies ? '1' : '0',
    'us_privacy': consentData.uspConsent ? consentData.uspConsent : '',
    'gpp': consentData.gpp ? consentData.gpp.gppString : '',
    'gpp_sid': consentData.gpp && Array.isArray(consentData.gpp.applicableSections)
      ? consentData.gpp.applicableSections.join(',') : ''
  }

  const existingUrl = new URL(url);
  const params = existingUrl.searchParams;

  for (const [key, value] of Object.entries(parameterMap)) {
    params.set(key, value);
  }

  existingUrl.search = params.toString();
  return existingUrl.toString();
};

function getSupportedEids(bid) {
  if (isArray(deepAccess(bid, 'userIdAsEids'))) {
    return bid.userIdAsEids.filter(eid => {
      return SUPPORTED_USER_ID_SOURCES.indexOf(eid.source) !== -1;
    });
  }
  return [];
}

function isSecure(bid) {
  return deepAccess(bid, 'params.bidOverride.imp.secure') || (document.location.protocol === 'https:') ? 1 : 0;
};

function getPubIdMode(bid) {
  let pubIdMode;
  if (deepAccess(bid, 'params.pubId')) {
    pubIdMode = true;
  } else if (deepAccess(bid, 'params.dcn') && deepAccess(bid, 'params.pos')) {
    pubIdMode = false;
  };
  return pubIdMode;
};

function getAdapterMode(bid) {
  let adapterMode = getConfigValue(bid, 'mode');
  adapterMode = adapterMode ? adapterMode.toLowerCase() : undefined;
  if (typeof adapterMode === 'undefined' || adapterMode === BANNER) {
    return BANNER;
  } else if (adapterMode === VIDEO) {
    return VIDEO;
  } else if (adapterMode === 'all') {
    return '*';
  }
};

function getResponseFormat(bid) {
  const adm = bid.adm;
  if (adm.indexOf('o2playerSettings') !== -1 || adm.indexOf('YAHOO.VideoPlatform.VideoPlayer') !== -1 || adm.indexOf('AdPlacement') !== -1) {
    return BANNER;
  } else if (adm.indexOf('VAST') !== -1) {
    return VIDEO;
  }
};

function getFloorModuleData(bid) {
  const adapterMode = getAdapterMode(bid);
  const getFloorRequestObject = {
    currency: deepAccess(bid, 'params.bidOverride.cur') || DEFAULT_CURRENCY,
    mediaType: adapterMode,
    size: '*'
  };
  return (isFn(bid.getFloor)) ? bid.getFloor(getFloorRequestObject) : false;
};

function filterBidRequestByMode(validBidRequests) {
  const mediaTypesMode = getAdapterMode(validBidRequests[0]);
  let result = [];
  if (mediaTypesMode === BANNER) {
    result = validBidRequests.filter(bid => {
      return Object.keys(bid.mediaTypes).some(item => item === BANNER);
    });
  } else if (mediaTypesMode === VIDEO) {
    result = validBidRequests.filter(bid => {
      return Object.keys(bid.mediaTypes).some(item => item === VIDEO);
    });
  } else if (mediaTypesMode === '*') {
    result = validBidRequests.filter(bid => {
      return Object.keys(bid.mediaTypes).some(item => item === BANNER || item === VIDEO);
    });
  };
  return result;
};

function validateAppendObject(validationType, allowedKeys, inputObject, appendToObject) {
  const outputObject = {
    ...appendToObject
  };

  for (const objectKey in inputObject) {
    switch (validationType) {
      case 'string':
        if (allowedKeys.indexOf(objectKey) !== -1 && isStr(inputObject[objectKey])) {
          outputObject[objectKey] = inputObject[objectKey];
        };
        break;
      case 'number':
        if (allowedKeys.indexOf(objectKey) !== -1 && isNumber(inputObject[objectKey])) {
          outputObject[objectKey] = inputObject[objectKey];
        };
        break;

      case 'array':
        if (allowedKeys.indexOf(objectKey) !== -1 && isArray(inputObject[objectKey])) {
          outputObject[objectKey] = inputObject[objectKey];
        };
        break;
      case 'object':
        if (allowedKeys.indexOf(objectKey) !== -1 && isPlainObject(inputObject[objectKey])) {
          outputObject[objectKey] = inputObject[objectKey];
        };
        break;
      case 'objectAllKeys':
        if (isPlainObject(inputObject)) {
          outputObject[objectKey] = inputObject[objectKey];
        };
        break;
    };
  };
  return outputObject;
};

function getTtl(bidderRequest) {
  const globalTTL = getConfigValue(bidderRequest, 'ttl');
  return globalTTL ? validateTTL(globalTTL) : validateTTL(deepAccess(bidderRequest, 'params.ttl'));
};

function validateTTL(ttl) {
  return (isNumber(ttl) && ttl > 0 && ttl < 3600) ? ttl : DEFAULT_BID_TTL
};

function isNotEmptyStr(value) {
  return (isStr(value) && value.length > 0);
};

function generateOpenRtbObject(bidderRequest, bid) {
  if (bidderRequest) {
    let outBoundBidRequest = {
      id: generateUUID(),
      cur: [getFloorModuleData(bidderRequest).currency || deepAccess(bid, 'params.bidOverride.cur') || DEFAULT_CURRENCY],
      imp: [],
      site: {
        page: deepAccess(bidderRequest, 'refererInfo.page'),
      },
      device: {
        dnt: 0,
        ua: navigator.userAgent,
        ip: deepAccess(bid, 'params.bidOverride.device.ip') || deepAccess(bid, 'params.ext.ip') || undefined,
        w: window.screen.width,
        h: window.screen.height
      },
      regs: {
        ext: {
          'us_privacy': bidderRequest.uspConsent ? bidderRequest.uspConsent : '',
          gdpr: bidderRequest.gdprConsent && bidderRequest.gdprConsent.gdprApplies ? 1 : 0,
          gpp: bidderRequest.gppConsent ? bidderRequest.gppConsent.gppString : '',
          gpp_sid: bidderRequest.gppConsent ? bidderRequest.gppConsent.applicableSections : []
        }
      },
      source: {
        ext: {
          hb: 1,
          adapterver: ADAPTER_VERSION,
          prebidver: PREBID_VERSION,
          integration: {
            name: INTEGRATION_METHOD,
            ver: PREBID_VERSION
          }
        },
        fd: 1
      },
      user: {
        ext: {
          consent: bidderRequest.gdprConsent && bidderRequest.gdprConsent.gdprApplies
            ? bidderRequest.gdprConsent.consentString : '',
          eids: getSupportedEids(bid)
        }
      }
    };

    if (getPubIdMode(bid) === true) {
      outBoundBidRequest.site.publisher = {
        id: bid.params.pubId
      }
      if (deepAccess(bid, 'params.bidOverride.site.id') || deepAccess(bid, 'params.siteId')) {
        outBoundBidRequest.site.id = deepAccess(bid, 'params.bidOverride.site.id') || bid.params.siteId;
      }
    } else {
      outBoundBidRequest.site.id = bid.params.dcn;
    };

    if (bidderRequest.ortb2?.regs?.gpp) {
      outBoundBidRequest.regs.ext.gpp = bidderRequest.ortb2.regs.gpp;
      outBoundBidRequest.regs.ext.gpp_sid = bidderRequest.ortb2.regs.gpp_sid
    };

    if (bidderRequest.ortb2) {
      outBoundBidRequest = appendFirstPartyData(outBoundBidRequest, bid);
    };

    const schainData = deepAccess(bid, 'schain.nodes');
    if (isArray(schainData) && schainData.length > 0) {
      outBoundBidRequest.source.ext.schain = bid.schain;
      outBoundBidRequest.source.ext.schain.nodes[0].rid = outBoundBidRequest.id;
    };

    return outBoundBidRequest;
  };
};

function appendImpObject(bid, openRtbObject) {
  const mediaTypeMode = getAdapterMode(bid);

  if (openRtbObject && bid) {
    const impObject = {
      id: bid.bidId,
      secure: isSecure(bid),
      bidfloor: getFloorModuleData(bid).floor || deepAccess(bid, 'params.bidOverride.imp.bidfloor')
    };

    if (bid.mediaTypes.banner && (typeof mediaTypeMode === 'undefined' || mediaTypeMode === BANNER || mediaTypeMode === '*')) {
      impObject.banner = {
        mimes: bid.mediaTypes.banner.mimes || ['text/html', 'text/javascript', 'application/javascript', 'image/jpg'],
        format: transformSizes(bid.sizes)
      };
      if (bid.mediaTypes.banner.pos) {
        impObject.banner.pos = bid.mediaTypes.banner.pos;
      };
    };

    if (bid.mediaTypes.video && (mediaTypeMode === VIDEO || mediaTypeMode === '*')) {
      const playerSize = transformSizes(bid.mediaTypes.video.playerSize);
      impObject.video = {
        mimes: deepAccess(bid, 'params.bidOverride.imp.video.mimes') || bid.mediaTypes.video.mimes || ['video/mp4', 'application/javascript'],
        w: deepAccess(bid, 'params.bidOverride.imp.video.w') || playerSize[0].w,
        h: deepAccess(bid, 'params.bidOverride.imp.video.h') || playerSize[0].h,
        maxbitrate: deepAccess(bid, 'params.bidOverride.imp.video.maxbitrate') || bid.mediaTypes.video.maxbitrate || undefined,
        maxduration: deepAccess(bid, 'params.bidOverride.imp.video.maxduration') || bid.mediaTypes.video.maxduration || undefined,
        minduration: deepAccess(bid, 'params.bidOverride.imp.video.minduration') || bid.mediaTypes.video.minduration || undefined,
        api: deepAccess(bid, 'params.bidOverride.imp.video.api') || bid.mediaTypes.video.api || [2],
        delivery: deepAccess(bid, 'params.bidOverride.imp.video.delivery') || bid.mediaTypes.video.delivery || undefined,
        pos: deepAccess(bid, 'params.bidOverride.imp.video.pos') || bid.mediaTypes.video.pos || undefined,
        playbackmethod: deepAccess(bid, 'params.bidOverride.imp.video.playbackmethod') || bid.mediaTypes.video.playbackmethod || undefined,
        placement: deepAccess(bid, 'params.bidOverride.imp.video.placement') || bid.mediaTypes.video.placement || undefined,
        linearity: deepAccess(bid, 'params.bidOverride.imp.video.linearity') || bid.mediaTypes.video.linearity || 1,
        protocols: deepAccess(bid, 'params.bidOverride.imp.video.protocols') || bid.mediaTypes.video.protocols || [2, 5],
        startdelay: deepAccess(bid, 'params.bidOverride.imp.video.startdelay') || bid.mediaTypes.video.startdelay || 0,
        rewarded: deepAccess(bid, 'params.bidOverride.imp.video.rewarded') || undefined,
      }
    }

    impObject.ext = {
      dfp_ad_unit_code: bid.adUnitCode
    };

    if (deepAccess(bid, 'params.kvp') && isPlainObject(bid.params.kvp)) {
      impObject.ext.kvs = {};
      for (const key in bid.params.kvp) {
        if (isStr(bid.params.kvp[key]) || isNumber(bid.params.kvp[key])) {
          impObject.ext.kvs[key] = bid.params.kvp[key];
        } else if (isArray(bid.params.kvp[key])) {
          const array = bid.params.kvp[key];
          if (array.every(value => isStr(value)) || array.every(value => isNumber(value))) {
            impObject.ext.kvs[key] = bid.params.kvp[key];
          }
        }
      }
    };

    if (deepAccess(bid, 'ortb2Imp.ext.data') && isPlainObject(bid.ortb2Imp.ext.data)) {
      impObject.ext.data = bid.ortb2Imp.ext.data;
    };

    if (deepAccess(bid, 'ortb2Imp.instl') && isNumber(bid.ortb2Imp.instl) && (bid.ortb2Imp.instl === 1)) {
      impObject.instl = bid.ortb2Imp.instl;
    };

    if (getPubIdMode(bid) === false) {
      impObject.tagid = bid.params.pos;
      impObject.ext.pos = bid.params.pos;
    } else if (deepAccess(bid, 'params.placementId')) {
      impObject.tagid = bid.params.placementId
    };

    openRtbObject.imp.push(impObject);
  };
};

function appendFirstPartyData(outBoundBidRequest, bid) {
  const ortb2Object = bid.ortb2;
  const siteObject = deepAccess(ortb2Object, 'site') || undefined;
  const siteContentObject = deepAccess(siteObject, 'content') || undefined;
  const sitePublisherObject = deepAccess(siteObject, 'publisher') || undefined;
  const siteContentDataArray = deepAccess(siteObject, 'content.data') || undefined;
  const appContentObject = deepAccess(ortb2Object, 'app.content') || undefined;
  const appContentDataArray = deepAccess(ortb2Object, 'app.content.data') || undefined;
  const userObject = deepAccess(ortb2Object, 'user') || undefined;

  if (siteObject && isPlainObject(siteObject)) {
    const allowedSiteStringKeys = ['name', 'domain', 'page', 'ref', 'keywords', 'search'];
    const allowedSiteArrayKeys = ['cat', 'sectioncat', 'pagecat'];
    const allowedSiteObjectKeys = ['ext'];
    outBoundBidRequest.site = validateAppendObject('string', allowedSiteStringKeys, siteObject, outBoundBidRequest.site);
    outBoundBidRequest.site = validateAppendObject('array', allowedSiteArrayKeys, siteObject, outBoundBidRequest.site);
    outBoundBidRequest.site = validateAppendObject('object', allowedSiteObjectKeys, siteObject, outBoundBidRequest.site);
  };

  if (sitePublisherObject && isPlainObject(sitePublisherObject)) {
    const allowedPublisherObjectKeys = ['ext'];
    outBoundBidRequest.site.publisher = validateAppendObject('object', allowedPublisherObjectKeys, sitePublisherObject, outBoundBidRequest.site.publisher);
  }

  if (siteContentObject && isPlainObject(siteContentObject)) {
    const allowedContentStringKeys = ['id', 'title', 'series', 'season', 'genre', 'contentrating', 'language'];
    const allowedContentNumberkeys = ['episode', 'prodq', 'context', 'livestream', 'len'];
    const allowedContentArrayKeys = ['cat'];
    const allowedContentObjectKeys = ['ext'];
    outBoundBidRequest.site.content = validateAppendObject('string', allowedContentStringKeys, siteContentObject, outBoundBidRequest.site.content);
    outBoundBidRequest.site.content = validateAppendObject('number', allowedContentNumberkeys, siteContentObject, outBoundBidRequest.site.content);
    outBoundBidRequest.site.content = validateAppendObject('array', allowedContentArrayKeys, siteContentObject, outBoundBidRequest.site.content);
    outBoundBidRequest.site.content = validateAppendObject('object', allowedContentObjectKeys, siteContentObject, outBoundBidRequest.site.content);

    if (siteContentDataArray && isArray(siteContentDataArray)) {
      siteContentDataArray.every(dataObject => {
        let newDataObject = {};
        const allowedContentDataStringKeys = ['id', 'name'];
        const allowedContentDataArrayKeys = ['segment'];
        const allowedContentDataObjectKeys = ['ext'];
        newDataObject = validateAppendObject('string', allowedContentDataStringKeys, dataObject, newDataObject);
        newDataObject = validateAppendObject('array', allowedContentDataArrayKeys, dataObject, newDataObject);
        newDataObject = validateAppendObject('object', allowedContentDataObjectKeys, dataObject, newDataObject);
        outBoundBidRequest.site.content.data = [];
        outBoundBidRequest.site.content.data.push(newDataObject);
      });
    };
  };

  if (appContentObject && isPlainObject(appContentObject)) {
    if (appContentDataArray && isArray(appContentDataArray)) {
      appContentDataArray.every(dataObject => {
        let newDataObject = {};
        const allowedContentDataStringKeys = ['id', 'name'];
        const allowedContentDataArrayKeys = ['segment'];
        const allowedContentDataObjectKeys = ['ext'];
        newDataObject = validateAppendObject('string', allowedContentDataStringKeys, dataObject, newDataObject);
        newDataObject = validateAppendObject('array', allowedContentDataArrayKeys, dataObject, newDataObject);
        newDataObject = validateAppendObject('object', allowedContentDataObjectKeys, dataObject, newDataObject);
        outBoundBidRequest.app = {
          content: {
            data: []
          }
        };
        outBoundBidRequest.app.content.data.push(newDataObject);
      });
    };
  };

  if (userObject && isPlainObject(userObject)) {
    const allowedUserStrings = ['id', 'buyeruid', 'gender', 'keywords', 'customdata'];
    const allowedUserNumbers = ['yob'];
    const allowedUserArrays = ['data'];
    const allowedUserObjects = ['ext'];
    outBoundBidRequest.user = validateAppendObject('string', allowedUserStrings, userObject, outBoundBidRequest.user);
    outBoundBidRequest.user = validateAppendObject('number', allowedUserNumbers, userObject, outBoundBidRequest.user);
    outBoundBidRequest.user = validateAppendObject('array', allowedUserArrays, userObject, outBoundBidRequest.user);
    outBoundBidRequest.user.ext = validateAppendObject('object', allowedUserObjects, userObject, outBoundBidRequest.user.ext);
  };

  return outBoundBidRequest;
};

function generateServerRequest({payload, requestOptions, bidderRequest}) {
  const pubIdMode = getPubIdMode(bidderRequest);
  const overrideEndpoint = getConfigValue(bidderRequest, 'endpoint');
  let sspEndpoint = overrideEndpoint || SSP_ENDPOINT_DCN_POS;

  if (pubIdMode === true) {
    sspEndpoint = overrideEndpoint || SSP_ENDPOINT_PUBID;
  };

  if (deepAccess(bidderRequest, 'params.testing.e2etest') === true) {
    logInfo('Adapter e2etest mode is active');
    requestOptions.withCredentials = false;

    if (pubIdMode === true) {
      payload.site.id = TEST_MODE_PUBID_DCN;
    } else {
      const mediaTypeMode = getAdapterMode(bidderRequest);
      payload.site.id = TEST_MODE_DCN;
      payload.imp.forEach(impObject => {
        impObject.ext.e2eTestMode = true;
        if (mediaTypeMode === BANNER) {
          impObject.tagid = TEST_MODE_BANNER_POS; // banner passback
        } else if (mediaTypeMode === VIDEO) {
          impObject.tagid = TEST_MODE_VIDEO_POS; // video passback
        } else {
          const bidderCode = bidderRequest.bidderCode;
          logWarn(`e2etest mode does not support ${bidderCode}.mode="all". \n Please specify either "banner" or "video"`);
          logWarn(`Adapter e2etest mode: Please make sure your adUnit matches the ${bidderCode}.mode video or banner`);
        }
      });
    }
  };

  return {
    url: sspEndpoint,
    method: 'POST',
    data: payload,
    options: requestOptions,
    bidderRequest // Additional data for use in interpretResponse()
  };
};

function createRenderer(bidderRequest, bidResponse) {
  const renderer = Renderer.install({
    url: 'https://s.yimg.com/kp/prebid-outstream-renderer/renderer.js',
    loaded: false,
    adUnitCode: bidderRequest.adUnitCode
  })

  try {
    renderer.setRender(function(bidResponse) {
      setTimeout(function() {
        // eslint-disable-next-line no-undef
        o2PlayerRender(bidResponse);
      }, deepAccess(bidderRequest, 'params.testing.renderer.setTimeout') || DEFAULT_RENDERER_TIMEOUT);
    });
  } catch (error) {
    logWarn('Renderer error: setRender() failed', error);
  }
  return renderer;
}

/* Utility functions */

export const spec = {
  code: BIDDER_CODE,
  gvlid: GVLID,
  aliases: BIDDER_ALIASES,
  supportedMediaTypes: [BANNER, VIDEO],

  isBidRequestValid: function(bid) {
    const params = bid.params;
    if (deepAccess(params, 'testing.e2etest') === true) {
      return true;
    } else if (
      isPlainObject(params) &&
      (isNotEmptyStr(params.pubId) || (isNotEmptyStr(params.dcn) && isNotEmptyStr(params.pos)))
    ) {
      return true;
    } else {
      logWarn('Bidder params missing or incorrect, please pass object with either: dcn & pos OR pubId');
      return false;
    }
  },

  buildRequests: function(validBidRequests, bidderRequest) {
    if (isEmpty(validBidRequests) || isEmpty(bidderRequest)) {
      logWarn('buildRequests called with either empty "validBidRequests" or "bidderRequest"');
      return undefined;
    };

    const requestOptions = {
      contentType: 'application/json',
      customHeaders: {
        'x-openrtb-version': '2.5'
      }
    };

    requestOptions.withCredentials = hasPurpose1Consent(bidderRequest.gdprConsent);

    const filteredBidRequests = filterBidRequestByMode(validBidRequests);

    if (getConfigValue(bidderRequest, 'singleRequestMode') === true) {
      const payload = generateOpenRtbObject(bidderRequest, filteredBidRequests[0]);
      filteredBidRequests.forEach(bid => {
        appendImpObject(bid, payload);
      });
      return [generateServerRequest({payload, requestOptions, bidderRequest})];
    }

    return filteredBidRequests.map(bid => {
      const payloadClone = generateOpenRtbObject(bidderRequest, bid);
      appendImpObject(bid, payloadClone);
      return generateServerRequest({payload: payloadClone, requestOptions, bidderRequest: bid});
    });
  },

  interpretResponse: function(serverResponse, { bidderRequest }) {
    const response = [];
    if (!serverResponse.body || !Array.isArray(serverResponse.body.seatbid)) {
      return response;
    }
    let seatbids = serverResponse.body.seatbid;
    seatbids.forEach(seatbid => {
      let bid;

      try {
        bid = seatbid.bid[0];
      } catch (e) {
        return response;
      }

      let cpm = (bid.ext && bid.ext.encp) ? bid.ext.encp : bid.price;

      let bidResponse = {
        adId: deepAccess(bid, 'adId') ? bid.adId : bid.impid || bid.crid,
        requestId: bid.impid,
        cpm: cpm,
        width: bid.w,
        height: bid.h,
        creativeId: bid.crid || 0,
        currency: bid.cur || DEFAULT_CURRENCY,
        dealId: bid.dealid ? bid.dealid : null,
        netRevenue: true,
        ttl: getTtl(bidderRequest),
        meta: {
          advertiserDomains: bid.adomain,
        }
      };

      const responseAdmFormat = getResponseFormat(bid);
      if (responseAdmFormat === BANNER) {
        bidResponse.mediaType = BANNER;
        bidResponse.ad = bid.adm;
        bidResponse.meta.mediaType = BANNER;
      } else if (responseAdmFormat === VIDEO) {
        bidResponse.mediaType = VIDEO;
        bidResponse.meta.mediaType = VIDEO;
        bidResponse.vastXml = bid.adm;

        if (bid.nurl) {
          bidResponse.vastUrl = bid.nurl;
        };
      }

      if (deepAccess(bidderRequest, 'mediaTypes.video.context') === 'outstream' && !bidderRequest.renderer) {
        bidResponse.renderer = createRenderer(bidderRequest, bidResponse) || undefined;
      }

      response.push(bidResponse);
    });

    return response;
  },

  getUserSyncs: function(syncOptions, serverResponses, gdprConsent, uspConsent, gppConsent) {
    const bidResponse = !isEmpty(serverResponses) && serverResponses[0].body;

    if (bidResponse && bidResponse.ext && bidResponse.ext.pixels) {
      const userSyncObjects = extractUserSyncUrls(syncOptions, bidResponse.ext.pixels);
      userSyncObjects.forEach(userSyncObject => {
        userSyncObject.url = updateConsentQueryParams(userSyncObject.url, {
          gpp: gppConsent,
          gdpr: gdprConsent,
          uspConsent: uspConsent
        });
      });
      return userSyncObjects;
    }

    return [];
  }
};

registerBidder(spec);