prebid/Prebid.js

View on GitHub
modules/cadentApertureMXBidAdapter.js

Summary

Maintainability
D
3 days
Test Coverage
import {
  _each,
  deepAccess, getBidIdParameter,
  isArray,
  isFn,
  isPlainObject,
  isStr,
  logError,
  logWarn
} from '../src/utils.js';
import {registerBidder} from '../src/adapters/bidderFactory.js';
import {BANNER, VIDEO} from '../src/mediaTypes.js';
import {Renderer} from '../src/Renderer.js';
import {find, includes} from '../src/polyfill.js';
import {parseDomain} from '../src/refererDetection.js';

const BIDDER_CODE = 'cadent_aperture_mx';
const ENDPOINT = 'hb.emxdgt.com';
const RENDERER_URL = 'https://js.brealtime.com/outstream/1.30.0/bundle.js';
const ADAPTER_VERSION = '1.5.1';
const DEFAULT_CUR = 'USD';
const ALIASES = [
  { code: 'emx_digital', gvlid: 183 },
  { code: 'cadent', gvlid: 183 },
];

const EIDS_SUPPORTED = [
  { key: 'idl_env', source: 'liveramp.com', rtiPartner: 'idl', queryParam: 'idl' },
  { key: 'uid2.id', source: 'uidapi.com', rtiPartner: 'UID2', queryParam: 'uid2' }
];

export const cadentAdapter = {
  validateSizes: (sizes) => {
    if (!isArray(sizes) || typeof sizes[0] === 'undefined') {
      logWarn(BIDDER_CODE + ': Sizes should be an array');
      return false;
    }
    return sizes.every(size => isArray(size) && size.length === 2);
  },
  checkVideoContext: (bid) => {
    return ((bid && bid.mediaTypes && bid.mediaTypes.video && bid.mediaTypes.video.context) && ((bid.mediaTypes.video.context === 'instream') || (bid.mediaTypes.video.context === 'outstream')));
  },
  buildBanner: (bid) => {
    let sizes = [];
    bid.mediaTypes && bid.mediaTypes.banner && bid.mediaTypes.banner.sizes ? sizes = bid.mediaTypes.banner.sizes : sizes = bid.sizes;
    if (!cadentAdapter.validateSizes(sizes)) {
      logWarn(BIDDER_CODE + ': could not detect mediaType banner sizes. Assigning to bid sizes instead');
      sizes = bid.sizes
    }
    return {
      format: sizes.map((size) => {
        return {
          w: size[0],
          h: size[1]
        };
      }),
      w: sizes[0][0],
      h: sizes[0][1]
    };
  },
  formatVideoResponse: (bidResponse, cadentBid, bidRequest) => {
    bidResponse.vastXml = cadentBid.adm;
    if (bidRequest.bidderRequest && bidRequest.bidderRequest.bids && bidRequest.bidderRequest.bids.length > 0) {
      const matchingBid = find(bidRequest.bidderRequest.bids, bid => bidResponse.requestId && bid.bidId && bidResponse.requestId === bid.bidId && bid.mediaTypes && bid.mediaTypes.video && bid.mediaTypes.video.context === 'outstream');
      if (matchingBid) {
        bidResponse.renderer = cadentAdapter.createRenderer(bidResponse, {
          id: cadentBid.id,
          url: RENDERER_URL
        });
      }
    }
    return bidResponse;
  },
  isMobile: () => {
    return (/(ios|ipod|ipad|iphone|android)/i).test(navigator.userAgent);
  },
  isConnectedTV: () => {
    return (/(smart[-]?tv|hbbtv|appletv|googletv|hdmi|netcast\.tv|viera|nettv|roku|\bdtv\b|sonydtv|inettvbrowser|\btv\b)/i).test(navigator.userAgent);
  },
  getDevice: () => {
    return {
      ua: navigator.userAgent,
      js: 1,
      dnt: (navigator.doNotTrack === 'yes' || navigator.doNotTrack === '1' || navigator.msDoNotTrack === '1') ? 1 : 0,
      h: screen.height,
      w: screen.width,
      devicetype: cadentAdapter.isMobile() ? 1 : cadentAdapter.isConnectedTV() ? 3 : 2,
      language: (navigator.language || navigator.browserLanguage || navigator.userLanguage || navigator.systemLanguage),
    };
  },
  cleanProtocols: (video) => {
    if (video.protocols && includes(video.protocols, 7)) {
      // not supporting VAST protocol 7 (VAST 4.0);
      logWarn(BIDDER_CODE + ': VAST 4.0 is currently not supported. This protocol has been filtered out of the request.');
      video.protocols = video.protocols.filter(protocol => protocol !== 7);
    }
    return video;
  },
  outstreamRender: (bid) => {
    bid.renderer.push(function () {
      let params = (bid && bid.params && bid.params[0] && bid.params[0].video) ? bid.params[0].video : {};
      window.emxVideoQueue = window.emxVideoQueue || [];
      window.queueEmxVideo({
        id: bid.adUnitCode,
        adsResponses: bid.vastXml,
        options: params
      });
      if (window.emxVideoReady && window.videojs) {
        window.emxVideoReady();
      }
    });
  },
  createRenderer: (bid, rendererParams) => {
    const renderer = Renderer.install({
      id: rendererParams.id,
      url: RENDERER_URL,
      loaded: false
    });
    try {
      renderer.setRender(cadentAdapter.outstreamRender);
    } catch (err) {
      logWarn('Prebid Error calling setRender on renderer', err);
    }

    return renderer;
  },
  buildVideo: (bid) => {
    let videoObj = Object.assign(bid.mediaTypes.video, bid.params.video);

    if (isArray(bid.mediaTypes.video.playerSize[0])) {
      videoObj['w'] = bid.mediaTypes.video.playerSize[0][0];
      videoObj['h'] = bid.mediaTypes.video.playerSize[0][1];
    } else {
      videoObj['w'] = bid.mediaTypes.video.playerSize[0];
      videoObj['h'] = bid.mediaTypes.video.playerSize[1];
    }
    return cadentAdapter.cleanProtocols(videoObj);
  },
  parseResponse: (bidResponseAdm) => {
    try {
      return decodeURIComponent(bidResponseAdm.replace(/%(?![0-9][0-9a-fA-F]+)/g, '%25'));
    } catch (err) {
      logError('cadent_aperture_mxBidAdapter', 'error', err);
    }
  },
  getSite: (refInfo) => {
    // TODO: do the fallbacks make sense?
    return {
      domain: refInfo.domain || parseDomain(refInfo.topmostLocation),
      page: refInfo.page || refInfo.topmostLocation,
      ref: refInfo.ref || window.document.referrer
    }
  },
  getGdpr: (bidRequests, cadentData) => {
    if (bidRequests.gdprConsent) {
      cadentData.regs = {
        ext: {
          gdpr: bidRequests.gdprConsent.gdprApplies === true ? 1 : 0
        }
      };
    }
    if (bidRequests.gdprConsent && bidRequests.gdprConsent.gdprApplies) {
      cadentData.user = {
        ext: {
          consent: bidRequests.gdprConsent.consentString
        }
      };
    }

    return cadentData;
  },

  getGpp: (bidRequest, cadentData) => {
    if (bidRequest.gppConsent) {
      const {gppString: gpp, applicableSections: gppSid} = bidRequest.gppConsent;
      if (cadentData.regs) {
        cadentData.regs.gpp = gpp;
        cadentData.regs.gpp_sid = gppSid;
      } else {
        cadentData.regs = {
          gpp: gpp,
          gpp_sid: gppSid
        }
      }
    }
    return cadentData;
  },
  getSupplyChain: (bidderRequest, cadentData) => {
    if (bidderRequest.bids[0] && bidderRequest.bids[0].schain) {
      cadentData.source = {
        ext: {
          schain: bidderRequest.bids[0].schain
        }
      };
    }

    return cadentData;
  },
  // supporting eids
  getEids(bidRequests) {
    return EIDS_SUPPORTED
      .map(cadentAdapter.getUserId(bidRequests))
      .filter(x => x);
  },
  getUserId(bidRequests) {
    return ({ key, source, rtiPartner }) => {
      let id = deepAccess(bidRequests, `userId.${key}`);
      return id ? cadentAdapter.formatEid(id, source, rtiPartner) : null;
    };
  },
  formatEid(id, source, rtiPartner) {
    return {
      source,
      uids: [{
        id,
        ext: { rtiPartner }
      }]
    };
  }
};

export const spec = {
  code: BIDDER_CODE,
  gvlid: 183,
  alias: ALIASES,
  supportedMediaTypes: [BANNER, VIDEO],
  isBidRequestValid: function (bid) {
    if (!bid || !bid.params) {
      logWarn(BIDDER_CODE + ': Missing bid or bid params.');
      return false;
    }

    if (bid.bidder !== BIDDER_CODE) {
      logWarn(BIDDER_CODE + ': Must use "cadent_aperture_mx" as bidder code.');
      return false;
    }

    if (!bid.params.tagid || !isStr(bid.params.tagid)) {
      logWarn(BIDDER_CODE + ': Missing tagid param or tagid present and not type String.');
      return false;
    }

    if (bid.mediaTypes && bid.mediaTypes.banner) {
      let sizes;
      bid.mediaTypes.banner.sizes ? sizes = bid.mediaTypes.banner.sizes : sizes = bid.sizes;
      if (!cadentAdapter.validateSizes(sizes)) {
        logWarn(BIDDER_CODE + ': Missing sizes in bid');
        return false;
      }
    } else if (bid.mediaTypes && bid.mediaTypes.video) {
      if (!cadentAdapter.checkVideoContext(bid)) {
        logWarn(BIDDER_CODE + ': Missing video context: instream or outstream');
        return false;
      }

      if (!bid.mediaTypes.video.playerSize) {
        logWarn(BIDDER_CODE + ': Missing video playerSize');
        return false;
      }
    }

    return true;
  },
  buildRequests: function (validBidRequests, bidderRequest) {
    const cadentImps = [];
    const timeout = bidderRequest.timeout || '';
    const timestamp = Date.now();
    const url = 'https://' + ENDPOINT + ('?t=' + timeout + '&ts=' + timestamp + '&src=pbjs');
    const secure = location.protocol.indexOf('https') > -1 ? 1 : 0;
    const device = cadentAdapter.getDevice();
    const site = cadentAdapter.getSite(bidderRequest.refererInfo);

    _each(validBidRequests, function (bid) {
      let tagid = getBidIdParameter('tagid', bid.params);
      let bidfloor = parseFloat(getBidFloor(bid)) || 0;
      let isVideo = !!bid.mediaTypes.video;
      let data = {
        id: bid.bidId,
        tid: bid.ortb2Imp?.ext?.tid,
        tagid,
        secure
      };

      // adding gpid support
      let gpid =
        deepAccess(bid, 'ortb2Imp.ext.gpid') ||
        deepAccess(bid, 'ortb2Imp.ext.data.adserver.adslot') ||
        deepAccess(bid, 'ortb2Imp.ext.data.pbadslot');

      if (gpid) {
        data.ext = { gpid: gpid.toString() };
      }
      let typeSpecifics = isVideo ? { video: cadentAdapter.buildVideo(bid) } : { banner: cadentAdapter.buildBanner(bid) };
      let bidfloorObj = bidfloor > 0 ? { bidfloor, bidfloorcur: DEFAULT_CUR } : {};
      let cadentBid = Object.assign(data, typeSpecifics, bidfloorObj);
      cadentImps.push(cadentBid);
    });

    let cadentData = {
      id: bidderRequest.auctionId ?? bidderRequest.bidderRequestId,
      imp: cadentImps,
      device,
      site,
      cur: DEFAULT_CUR,
      version: ADAPTER_VERSION
    };

    cadentData = cadentAdapter.getGdpr(bidderRequest, Object.assign({}, cadentData));
    cadentData = cadentAdapter.getGpp(bidderRequest, Object.assign({}, cadentData));
    cadentData = cadentAdapter.getSupplyChain(bidderRequest, Object.assign({}, cadentData));
    if (bidderRequest && bidderRequest.uspConsent) {
      cadentData.us_privacy = bidderRequest.uspConsent;
    }

    // adding eid support
    if (bidderRequest.userId) {
      let eids = cadentAdapter.getEids(bidderRequest);
      if (eids.length > 0) {
        if (cadentData.user && cadentData.user.ext) {
          cadentData.user.ext.eids = eids;
        } else {
          cadentData.user = {
            ext: {eids}
          };
        }
      }
    }

    return {
      method: 'POST',
      url,
      data: JSON.stringify(cadentData),
      options: {
        withCredentials: true
      },
      bidderRequest
    };
  },
  interpretResponse: function (serverResponse, bidRequest) {
    let cadentBidResponses = [];
    let response = serverResponse.body || {};
    if (response.seatbid && response.seatbid.length > 0 && response.seatbid[0].bid) {
      response.seatbid.forEach(function (cadentBid) {
        cadentBid = cadentBid.bid[0];
        let isVideo = false;
        let adm = cadentAdapter.parseResponse(cadentBid.adm) || '';
        let bidResponse = {
          requestId: cadentBid.id,
          cpm: cadentBid.price,
          width: cadentBid.w,
          height: cadentBid.h,
          creativeId: cadentBid.crid || cadentBid.id,
          dealId: cadentBid.dealid || null,
          currency: 'USD',
          netRevenue: true,
          ttl: cadentBid.ttl,
          ad: adm
        };
        if (cadentBid.adm && cadentBid.adm.indexOf('<?xml version=') > -1) {
          isVideo = true;
          bidResponse = cadentAdapter.formatVideoResponse(bidResponse, Object.assign({}, cadentBid), bidRequest);
        }
        bidResponse.mediaType = (isVideo ? VIDEO : BANNER);

        // support for adomain in prebid 5.0
        if (cadentBid.adomain && cadentBid.adomain.length) {
          bidResponse.meta = {
            advertiserDomains: cadentBid.adomain
          };
        }

        cadentBidResponses.push(bidResponse);
      });
    }
    return cadentBidResponses;
  },
  getUserSyncs: function (syncOptions, responses, gdprConsent, uspConsent, gppConsent) {
    const syncs = [];
    const consentParams = [];
    if (syncOptions.iframeEnabled) {
      let url = 'https://biddr.brealtime.com/check.html';
      if (gdprConsent && typeof gdprConsent.consentString === 'string') {
        // add 'gdpr' only if 'gdprApplies' is defined
        if (typeof gdprConsent.gdprApplies === 'boolean') {
          consentParams.push(`gdpr=${Number(gdprConsent.gdprApplies)}&gdpr_consent=${gdprConsent.consentString}`);
        } else {
          consentParams.push(`?gdpr_consent=${gdprConsent.consentString}`);
        }
      }
      if (uspConsent && typeof uspConsent.consentString === 'string') {
        consentParams.push(`usp=${uspConsent.consentString}`);
      }
      if (gppConsent && typeof gppConsent === 'object') {
        if (gppConsent.gppString && typeof gppConsent.gppString === 'string') {
          consentParams.push(`gpp=${gppConsent.gppString}`);
        }
        if (gppConsent.applicableSections && typeof gppConsent.applicableSections === 'object') {
          consentParams.push(`gpp_sid=${gppConsent.applicableSections}`);
        }
      }
      if (consentParams.length > 0) {
        url = url + '?' + consentParams.join('&');
      }
      syncs.push({
        type: 'iframe',
        url: url
      });
    }
    return syncs;
  }
};

// support floors module in prebid 5.0
function getBidFloor(bid) {
  if (!isFn(bid.getFloor)) {
    return parseFloat(getBidIdParameter('bidfloor', bid.params));
  }

  let floor = bid.getFloor({
    currency: DEFAULT_CUR,
    mediaType: '*',
    size: '*'
  });
  if (isPlainObject(floor) && !isNaN(floor.floor) && floor.currency === 'USD') {
    return floor.floor;
  }
  return null;
}

registerBidder(spec);