prebid/Prebid.js

View on GitHub
modules/sovrnBidAdapter.js

Summary

Maintainability
F
4 days
Test Coverage
import {
  _each,
  isArray,
  getUniqueIdentifierStr,
  deepSetValue,
  logError,
  deepAccess,
  isInteger,
  logWarn, getBidIdParameter, isEmptyStr
} from '../src/utils.js';
import { registerBidder } from '../src/adapters/bidderFactory.js'
import {
  ADPOD,
  BANNER,
  VIDEO
} from '../src/mediaTypes.js'

/**
 * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid
 */

const ORTB_VIDEO_PARAMS = {
  'mimes': (value) => Array.isArray(value) && value.length > 0 && value.every(v => typeof v === 'string'),
  'minduration': (value) => isInteger(value),
  'maxduration': (value) => isInteger(value),
  'protocols': (value) => Array.isArray(value) && value.every(v => v >= 1 && v <= 10),
  'w': (value) => isInteger(value),
  'h': (value) => isInteger(value),
  'startdelay': (value) => isInteger(value),
  'placement': (value) => isInteger(value) && value >= 1 && value <= 5,
  'linearity': (value) => [1, 2].indexOf(value) !== -1,
  'skip': (value) => [0, 1].indexOf(value) !== -1,
  'skipmin': (value) => isInteger(value),
  'skipafter': (value) => isInteger(value),
  'sequence': (value) => isInteger(value),
  'battr': (value) => Array.isArray(value) && value.every(v => v >= 1 && v <= 17),
  'maxextended': (value) => isInteger(value),
  'minbitrate': (value) => isInteger(value),
  'maxbitrate': (value) => isInteger(value),
  'boxingallowed': (value) => [0, 1].indexOf(value) !== -1,
  'playbackmethod': (value) => Array.isArray(value) && value.every(v => v >= 1 && v <= 6),
  'playbackend': (value) => [1, 2, 3].indexOf(value) !== -1,
  'delivery': (value) => Array.isArray(value) && value.every(v => v >= 1 && v <= 3),
  'pos': (value) => isInteger(value) && value >= 1 && value <= 7,
  'api': (value) => Array.isArray(value) && value.every(v => v >= 1 && v <= 6)
}

const REQUIRED_VIDEO_PARAMS = {
  context: (value) => value !== ADPOD,
  mimes: ORTB_VIDEO_PARAMS.mimes,
  maxduration: ORTB_VIDEO_PARAMS.maxduration,
  protocols: ORTB_VIDEO_PARAMS.protocols
}

export const spec = {
  code: 'sovrn',
  supportedMediaTypes: [BANNER, VIDEO],
  gvlid: 13,

  /**
   * Check if the bid is a valid zone ID in either number or string form
   * @param {object} bid the Sovrn bid to validate
   * @return boolean for whether or not a bid is valid
   */
  isBidRequestValid: function (bid) {
    const video = bid?.mediaTypes?.video
    return !!(
      bid.params.tagid &&
      !isNaN(parseFloat(bid.params.tagid)) &&
      isFinite(bid.params.tagid) && (
        !video || (
          Object.keys(REQUIRED_VIDEO_PARAMS)
            .every(key => REQUIRED_VIDEO_PARAMS[key](video[key]))
        )
      )
    )
  },

  /**
   * Format the bid request object for our endpoint
   * @return object of parameters for Prebid AJAX request
   * @param bidReqs
   * @param bidderRequest
   */
  buildRequests: function(bidReqs, bidderRequest) {
    try {
      let sovrnImps = [];
      let iv;
      let schain;
      let eids;
      let criteoId;

      _each(bidReqs, function (bid) {
        if (!eids && bid.userIdAsEids) {
          eids = bid.userIdAsEids;
          eids.forEach(function (id) {
            if (id.uids && id.uids[0]) {
              if (id.source === 'criteo.com') {
                criteoId = id.uids[0].id
              }
            }
          })
        }

        if (bid.schain) {
          schain = schain || bid.schain
        }
        iv = iv || getBidIdParameter('iv', bid.params)

        const imp = {
          adunitcode: bid.adUnitCode,
          id: bid.bidId,
          tagid: String(getBidIdParameter('tagid', bid.params)),
          bidfloor: _getBidFloors(bid)
        }

        if (deepAccess(bid, 'mediaTypes.banner')) {
          let bidSizes = deepAccess(bid, 'mediaTypes.banner.sizes') || bid.sizes
          bidSizes = (isArray(bidSizes) && isArray(bidSizes[0])) ? bidSizes : [bidSizes]
          bidSizes = bidSizes.filter(size => isArray(size))
          const processedSizes = bidSizes.map(size => ({w: parseInt(size[0], 10), h: parseInt(size[1], 10)}))

          imp.banner = {
            format: processedSizes,
            w: 1,
            h: 1,
          };
        }
        if (deepAccess(bid, 'mediaTypes.video')) {
          imp.video = _buildVideoRequestObj(bid);
        }

        imp.ext = getBidIdParameter('ext', bid.ortb2Imp) || undefined

        const segmentsString = getBidIdParameter('segments', bid.params)
        if (segmentsString) {
          imp.ext = imp.ext || {}
          imp.ext.deals = segmentsString.split(',').map(deal => deal.trim())
        }

        const auctionEnvironment = bid?.ortb2Imp?.ext?.ae
        if (bidderRequest.fledgeEnabled && isInteger(auctionEnvironment)) {
          imp.ext = imp.ext || {}
          imp.ext.ae = auctionEnvironment
        } else {
          if (imp.ext?.ae) {
            delete imp.ext.ae
          }
        }

        sovrnImps.push(imp)
      })

      const fpd = bidderRequest.ortb2 || {};

      const site = fpd.site || {}
      site.page = bidderRequest.refererInfo.page
      site.domain = bidderRequest.refererInfo.domain

      const tmax = deepAccess(bidderRequest, 'timeout');

      const sovrnBidReq = {
        id: getUniqueIdentifierStr(),
        imp: sovrnImps,
        site: site,
        user: fpd.user || {},
        tmax: tmax
      }

      if (schain) {
        sovrnBidReq.source = {
          ext: {
            schain
          }
        };
      }

      const tid = deepAccess(bidderRequest, 'ortb2.source.tid')
      if (tid) {
        deepSetValue(sovrnBidReq, 'source.tid', tid)
      }

      const coppa = deepAccess(bidderRequest, 'ortb2.regs.coppa');
      if (coppa) {
        deepSetValue(sovrnBidReq, 'regs.coppa', 1);
      }

      if (bidderRequest.gdprConsent) {
        deepSetValue(sovrnBidReq, 'regs.ext.gdpr', +bidderRequest.gdprConsent.gdprApplies);
        deepSetValue(sovrnBidReq, 'user.ext.consent', bidderRequest.gdprConsent.consentString)
      }
      if (bidderRequest.uspConsent) {
        deepSetValue(sovrnBidReq, 'regs.ext.us_privacy', bidderRequest.uspConsent);
      }
      if (bidderRequest.gppConsent) {
        deepSetValue(sovrnBidReq, 'regs.gpp', bidderRequest.gppConsent.gppString);
        deepSetValue(sovrnBidReq, 'regs.gpp_sid', bidderRequest.gppConsent.applicableSections);
      }

      if (eids) {
        deepSetValue(sovrnBidReq, 'user.ext.eids', eids)
        if (criteoId) {
          deepSetValue(sovrnBidReq, 'user.ext.prebid_criteoid', criteoId)
        }
      }

      let url = `https://ap.lijit.com/rtb/bid?src=$$REPO_AND_VERSION$$`;
      if (iv) url += `&iv=${iv}`;

      return {
        method: 'POST',
        url: url,
        data: JSON.stringify(sovrnBidReq),
        options: {contentType: 'text/plain'}
      }
    } catch (e) {
      logError('Could not build bidrequest, error deatils:', e);
    }
  },

  /**
   * Format Sovrn responses as Prebid bid responses
   * @param {id, seatbid, ext} sovrnResponse A successful response from Sovrn.
   * @return An array of formatted bids (+ fledgeAuctionConfigs if available)
   */
  interpretResponse: function({ body: {id, seatbid, ext} }) {
    if (!id || !seatbid || !Array.isArray(seatbid)) return []

    try {
      let bids = seatbid
        .filter(seat => seat)
        .map(seat => seat.bid.map(sovrnBid => {
          const bid = {
            requestId: sovrnBid.impid,
            cpm: parseFloat(sovrnBid.price),
            width: parseInt(sovrnBid.w),
            height: parseInt(sovrnBid.h),
            creativeId: sovrnBid.crid || sovrnBid.id,
            dealId: sovrnBid.dealid || null,
            currency: 'USD',
            netRevenue: true,
            mediaType: sovrnBid.nurl ? BANNER : VIDEO,
            ttl: sovrnBid.ext?.ttl || 90,
            meta: { advertiserDomains: sovrnBid && sovrnBid.adomain ? sovrnBid.adomain : [] }
          }

          if (sovrnBid.nurl) {
            bid.ad = decodeURIComponent(`${sovrnBid.adm}<img src="${sovrnBid.nurl}">`)
          } else {
            bid.vastXml = decodeURIComponent(sovrnBid.adm)
          }

          return bid
        }))
        .flat()

      let fledgeAuctionConfigs = null;
      if (isArray(ext?.igbid)) {
        const seller = ext.seller
        const decisionLogicUrl = ext.decisionLogicUrl
        const sellerTimeout = ext.sellerTimeout
        ext.igbid.filter(item => isValidIgBid(item)).forEach((igbid) => {
          const perBuyerSignals = {}
          igbid.igbuyer.filter(item => isValidIgBuyer(item)).forEach(buyerItem => {
            perBuyerSignals[buyerItem.igdomain] = buyerItem.buyerdata
          })
          const interestGroupBuyers = [...Object.keys(perBuyerSignals)]
          if (interestGroupBuyers.length) {
            fledgeAuctionConfigs = fledgeAuctionConfigs || {}
            fledgeAuctionConfigs[igbid.impid] = {
              seller,
              decisionLogicUrl,
              sellerTimeout,
              interestGroupBuyers: interestGroupBuyers,
              perBuyerSignals,
            }
          }
        })
      }
      if (fledgeAuctionConfigs) {
        fledgeAuctionConfigs = Object.entries(fledgeAuctionConfigs).map(([bidId, cfg]) => {
          return {
            bidId,
            config: Object.assign({
              auctionSignals: {}
            }, cfg)
          }
        })
        return {
          bids,
          fledgeAuctionConfigs,
        }
      }
      return bids
    } catch (e) {
      logError('Could not interpret bidresponse, error details:', e)
      return e
    }
  },

  getUserSyncs: function(syncOptions, serverResponses, gdprConsent, uspConsent, gppConsent) {
    try {
      const tracks = []
      if (serverResponses && serverResponses.length !== 0) {
        if (syncOptions.iframeEnabled) {
          const iidArr = serverResponses.filter(resp => deepAccess(resp, 'body.ext.iid'))
            .map(resp => resp.body.ext.iid);
          const params = [];
          if (gdprConsent && gdprConsent.gdprApplies && typeof gdprConsent.consentString === 'string') {
            params.push(['gdpr_consent', gdprConsent.consentString]);
          }
          if (uspConsent) {
            params.push(['us_privacy', uspConsent]);
          }
          if (gppConsent) {
            params.push(['gpp', gppConsent.gppString]);
            params.push(['gpp_sid', gppConsent.applicableSections])
          }

          if (iidArr[0]) {
            params.push(['informer', iidArr[0]]);
            tracks.push({
              type: 'iframe',
              url: 'https://ce.lijit.com/beacon?' + params.map(p => p.join('=')).join('&')
            });
          }
        }

        if (syncOptions.pixelEnabled) {
          serverResponses.filter(resp => deepAccess(resp, 'body.ext.sync.pixels'))
            .reduce((acc, resp) => acc.concat(resp.body.ext.sync.pixels), [])
            .map(pixel => pixel.url)
            .forEach(url => tracks.push({ type: 'image', url }))
        }
      }
      return tracks
    } catch (e) {
      return []
    }
  },
}

function _buildVideoRequestObj(bid) {
  const videoObj = {}
  const bidSizes = deepAccess(bid, 'sizes')
  const videoAdUnitParams = deepAccess(bid, 'mediaTypes.video', {})
  const videoBidderParams = deepAccess(bid, 'params.video', {})
  const computedParams = {}

  if (bidSizes) {
    const sizes = (Array.isArray(bidSizes[0])) ? bidSizes[0] : bidSizes
    computedParams.w = sizes[0]
    computedParams.h = sizes[1]
  } else if (Array.isArray(videoAdUnitParams.playerSize)) {
    const sizes = (Array.isArray(videoAdUnitParams.playerSize[0])) ? videoAdUnitParams.playerSize[0] : videoAdUnitParams.playerSize
    computedParams.w = sizes[0]
    computedParams.h = sizes[1]
  }

  const videoParams = {
    ...computedParams,
    ...videoAdUnitParams,
    ...videoBidderParams
  };

  Object.keys(ORTB_VIDEO_PARAMS).forEach(paramName => {
    if (videoParams.hasOwnProperty(paramName)) {
      if (ORTB_VIDEO_PARAMS[paramName](videoParams[paramName])) {
        videoObj[paramName] = videoParams[paramName]
      } else {
        logWarn(`The OpenRTB video param ${paramName} has been skipped due to misformating. Please refer to OpenRTB 2.5 spec.`);
      }
    }
  })
  return videoObj
}

function _getBidFloors(bid) {
  const floorInfo = (bid.getFloor && typeof bid.getFloor === 'function') ? bid.getFloor({
    currency: 'USD',
    mediaType: bid.mediaTypes && bid.mediaTypes.banner ? 'banner' : 'video',
    size: '*'
  }) : {}
  const floorModuleValue = parseFloat(floorInfo.floor)
  if (!isNaN(floorModuleValue)) {
    return floorModuleValue
  }
  const paramValue = parseFloat(getBidIdParameter('bidfloor', bid.params))
  return !isNaN(paramValue) ? paramValue : undefined
}

function isValidIgBid(igBid) {
  return !isEmptyStr(igBid.impid) && isArray(igBid.igbuyer) && igBid.igbuyer.length
}

function isValidIgBuyer(igBuyer) {
  return !isEmptyStr(igBuyer.igdomain)
}

registerBidder(spec)