prebid/Prebid.js

View on GitHub
modules/marsmediaBidAdapter.js

Summary

Maintainability
F
1 wk
Test Coverage

'use strict';
import { deepAccess, getDNT, parseSizesInput, isArray, getWindowTop, deepSetValue, triggerPixel, getWindowSelf } from '../src/utils.js';
import {registerBidder} from '../src/adapters/bidderFactory.js';
import { BANNER, VIDEO } from '../src/mediaTypes.js';
import {config} from '../src/config.js';

function MarsmediaAdapter() {
  this.code = 'marsmedia';
  this.aliases = ['mars'];
  this.supportedMediaTypes = [VIDEO, BANNER];

  let SUPPORTED_VIDEO_PROTOCOLS = [2, 3, 5, 6];
  let SUPPORTED_VIDEO_MIMES = ['video/mp4'];
  let SUPPORTED_VIDEO_PLAYBACK_METHODS = [1, 2, 3, 4];
  let SUPPORTED_VIDEO_DELIVERY = [1];
  let SUPPORTED_VIDEO_API = [1, 2, 5];
  let slotsToBids = {};
  let version = '2.5';

  this.isBidRequestValid = function (bid) {
    return !!(bid.params && bid.params.zoneId);
  };

  this.getUserSyncs = function (syncOptions, responses, gdprConsent) {
    return [];
  };

  function frameImp(BRs, bidderRequest) {
    var impList = [];
    var isSecure = 0;
    if (bidderRequest && bidderRequest.refererInfo && bidderRequest.refererInfo.stack.length) {
      // clever trick to get the protocol
      // TODO: this should probably use parseUrl
      var el = document.createElement('a');
      el.href = bidderRequest.refererInfo.stack[0];
      isSecure = (el.protocol == 'https:') ? 1 : 0;
    }
    for (var i = 0; i < BRs.length; i++) {
      slotsToBids[BRs[i].adUnitCode] = BRs[i];
      var impObj = {};
      impObj.id = BRs[i].adUnitCode;
      impObj.secure = isSecure;

      if (deepAccess(BRs[i], 'mediaTypes.banner') || deepAccess(BRs[i], 'mediaType') === 'banner') {
        let banner = frameBanner(BRs[i]);
        if (banner) {
          impObj.banner = banner;
        }
      }
      if (deepAccess(BRs[i], 'mediaTypes.video') || deepAccess(BRs[i], 'mediaType') === 'video') {
        impObj.video = frameVideo(BRs[i]);
      }
      if (!(impObj.banner || impObj.video)) {
        continue;
      }
      impObj.bidfloor = _getFloor(BRs[i]);
      impObj.ext = frameExt(BRs[i]);
      impList.push(impObj);
    }
    return impList;
  }

  function frameSite(bidderRequest) {
    var site = {
      domain: '',
      page: '',
      ref: ''
    }
    if (bidderRequest && bidderRequest.refererInfo) {
      var ri = bidderRequest.refererInfo;
      // TODO: is 'ref' the right value here?
      site.ref = ri.ref;

      if (ri.stack.length) {
        site.page = ri.stack[ri.stack.length - 1];

        // clever trick to get the domain
        // TODO: does this logic make sense? why should domain be set to the lowermost frame's?
        // TODO: this should probably use parseUrl
        var el = document.createElement('a');
        el.href = ri.stack[0];
        site.domain = el.hostname;
      }
    }
    return site;
  }

  function frameDevice() {
    return {
      ua: navigator.userAgent,
      ip: '', // Empty Ip string is required, server gets the ip from HTTP header
      dnt: getDNT() ? 1 : 0,
    }
  }

  function getValidSizeSet(dimensionList) {
    let w = parseInt(dimensionList[0]);
    let h = parseInt(dimensionList[1]);
    // clever check for NaN
    if (! (w !== w || h !== h)) {  // eslint-disable-line
      return [w, h];
    }
    return false;
  }

  function frameBanner(adUnit) {
    // adUnit.sizes is scheduled to be deprecated, continue its support but prefer adUnit.mediaTypes.banner
    var sizeList = adUnit.sizes;
    if (adUnit.mediaTypes && adUnit.mediaTypes.banner) {
      sizeList = adUnit.mediaTypes.banner.sizes;
    }
    var sizeStringList = parseSizesInput(sizeList);
    var format = [];
    sizeStringList.forEach(function(size) {
      if (size) {
        var dimensionList = getValidSizeSet(size.split('x'));
        if (dimensionList) {
          format.push({
            'w': dimensionList[0],
            'h': dimensionList[1],
          });
        }
      }
    });
    if (format.length) {
      return {
        'format': format
      };
    }

    return false;
  }

  function frameVideo(bid) {
    var size = [];
    if (deepAccess(bid, 'mediaTypes.video.playerSize')) {
      var dimensionSet = bid.mediaTypes.video.playerSize;
      if (isArray(bid.mediaTypes.video.playerSize[0])) {
        dimensionSet = bid.mediaTypes.video.playerSize[0];
      }
      var validSize = getValidSizeSet(dimensionSet)
      if (validSize) {
        size = validSize;
      }
    }
    return {
      mimes: deepAccess(bid, 'mediaTypes.video.mimes') || SUPPORTED_VIDEO_MIMES,
      protocols: deepAccess(bid, 'mediaTypes.video.protocols') || SUPPORTED_VIDEO_PROTOCOLS,
      w: size[0],
      h: size[1],
      startdelay: deepAccess(bid, 'mediaTypes.video.startdelay') || 0,
      skip: deepAccess(bid, 'mediaTypes.video.skip') || 0,
      playbackmethod: deepAccess(bid, 'mediaTypes.video.playbackmethod') || SUPPORTED_VIDEO_PLAYBACK_METHODS,
      delivery: deepAccess(bid, 'mediaTypes.video.delivery') || SUPPORTED_VIDEO_DELIVERY,
      api: deepAccess(bid, 'mediaTypes.video.api') || SUPPORTED_VIDEO_API,
    }
  }

  function frameExt(bid) {
    if ((bid.mediaTypes && bid.mediaTypes.banner && bid.mediaTypes.banner.sizes)) {
      let bidSizes = (bid.mediaTypes && bid.mediaTypes.banner && 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)}));

      const element = document.getElementById(bid.adUnitCode);
      const minSize = _getMinSize(processedSizes);
      const viewabilityAmount = _isViewabilityMeasurable(element)
        ? _getViewability(element, getWindowTop(), minSize)
        : 'na';
      const viewabilityAmountRounded = isNaN(viewabilityAmount) ? viewabilityAmount : Math.round(viewabilityAmount);

      return {
        bidder: {
          zoneId: bid.params['zoneId']
        },
        viewability: viewabilityAmountRounded
      }
    } else {
      return {
        bidder: {
          zoneId: bid.params['zoneId']
        },
        viewability: 'na'
      }
    }
  }

  function frameBid(BRs, bidderRequest) {
    let bid = {
      id: BRs[0].bidderRequestId,
      imp: frameImp(BRs, bidderRequest),
      site: frameSite(bidderRequest),
      device: frameDevice(),
      user: {
        ext: {
          consent: deepAccess(bidderRequest, 'gdprConsent.gdprApplies') ? bidderRequest.gdprConsent.consentString : ''
        }
      },
      at: 1,
      tmax: 650,
      regs: {
        ext: {
          gdpr: deepAccess(bidderRequest, 'gdprConsent.gdprApplies') ? Boolean(bidderRequest.gdprConsent.gdprApplies & 1) : false
        }
      }
    };
    if (BRs[0].schain) {
      deepSetValue(bid, 'source.ext.schain', BRs[0].schain);
    }
    if (bidderRequest.uspConsent) {
      deepSetValue(bid, 'regs.ext.us_privacy', bidderRequest.uspConsent)
    }
    if (config.getConfig('coppa') === true) {
      deepSetValue(bid, 'regs.coppa', config.getConfig('coppa') & 1)
    }

    return bid;
  }

  function getFirstParam(key, validBidRequests) {
    for (let i = 0; i < validBidRequests.length; i++) {
      if (validBidRequests[i].params && validBidRequests[i].params[key]) {
        return validBidRequests[i].params[key];
      }
    }
  }

  this.buildRequests = function (BRs, bidderRequest) {
    let fallbackZoneId = getFirstParam('zoneId', BRs);
    if (fallbackZoneId === undefined || BRs.length < 1) {
      return [];
    }

    var uri = 'https://hb.go2speed.media/bidder/?bid=3mhdom&zoneId=' + fallbackZoneId;

    var fat = /(^v|(\.0)+$)/gi;
    var prebidVersion = '$prebid.version$';
    uri += '&hbv=' + prebidVersion.replace(fat, '') + ',' + version.replace(fat, '');

    var bidRequest = frameBid(BRs, bidderRequest);
    if (!bidRequest.imp.length) {
      return {};
    }

    return {
      method: 'POST',
      url: uri,
      data: JSON.stringify(bidRequest)
    };
  };

  this.onBidWon = function (bid) {
    if (typeof bid.nurl !== 'undefined') {
      const cpm = bid.pbMg;
      bid.nurl = bid.nurl.replace(
        /\$\{AUCTION_PRICE\}/,
        cpm
      );
      triggerPixel(bid.nurl, null);
    };
    sendbeacon(bid, 17)
  };

  this.onTimeout = function (bid) {
    sendbeacon(bid, 19)
  };

  this.onSetTargeting = function (bid) {
    sendbeacon(bid, 20)
  };

  this.interpretResponse = function (serverResponse) {
    let responses = serverResponse.body || [];
    let bids = [];
    let i = 0;

    if (responses.seatbid) {
      let temp = [];
      for (i = 0; i < responses.seatbid.length; i++) {
        for (let j = 0; j < responses.seatbid[i].bid.length; j++) {
          temp.push(responses.seatbid[i].bid[j]);
        }
      }
      responses = temp;
    }

    for (i = 0; i < responses.length; i++) {
      let bid = responses[i];
      let bidRequest = slotsToBids[bid.impid];
      let bidResponse = {
        requestId: bidRequest.bidId,
        cpm: parseFloat(bid.price),
        width: bid.w,
        height: bid.h,
        creativeId: bid.crid,
        currency: 'USD',
        netRevenue: true,
        ttl: 350,
        nurl: bid.nurl
      };

      if (bidRequest.mediaTypes && bidRequest.mediaTypes.video) {
        if (bid.adm.charAt(0) === '<') {
          bidResponse.vastXml = bid.adm;
        } else {
          bidResponse.vastUrl = bid.adm;
        }
        bidResponse.mediaType = 'video';
        bidResponse.ttl = 600;
      } else {
        bidResponse.ad = bid.adm;
      }

      bids.push(bidResponse);
    }

    return bids;
  };

  function sendbeacon(bid, type) {
    const bidString = JSON.stringify(bid);
    const encodedBuf = window.btoa(bidString);
    triggerPixel('https://ping-hqx-1.go2speed.media/notification/rtb/beacon/?bt=' + type + '&bid=3mhdom&hb_j=' + encodedBuf, null);
  }

  /**
   * Gets bidfloor
   * @param {Object} bid
   * @returns {Number} floor
   */
  function _getFloor (bid) {
    const curMediaType = bid.mediaTypes.video ? 'video' : 'banner';
    let floor = 0;

    if (typeof bid.getFloor === 'function') {
      const floorInfo = bid.getFloor({
        currency: 'USD',
        mediaType: curMediaType,
        size: '*'
      });

      if (typeof floorInfo === 'object' &&
        floorInfo.currency === 'USD' &&
        !isNaN(parseFloat(floorInfo.floor))) {
        floor = floorInfo.floor;
      }
    }

    return floor;
  }

  function _getMinSize(sizes) {
    return sizes.reduce((min, size) => size.h * size.w < min.h * min.w ? size : min);
  }

  function _isViewabilityMeasurable(element) {
    return !_isIframe() && element !== null;
  }

  function _isIframe() {
    try {
      return getWindowSelf() !== getWindowTop();
    } catch (e) {
      return true;
    }
  }

  function _getViewability(element, topWin, { w, h } = {}) {
    return topWin.document.visibilityState === 'visible'
      ? _getPercentInView(element, topWin, { w, h })
      : 0;
  }

  function _getPercentInView(element, topWin, { w, h } = {}) {
    const elementBoundingBox = _getBoundingBox(element, { w, h });

    const elementInViewBoundingBox = _getIntersectionOfRects([ {
      left: 0,
      top: 0,
      right: topWin.innerWidth,
      bottom: topWin.innerHeight
    }, elementBoundingBox ]);

    let elementInViewArea, elementTotalArea;

    if (elementInViewBoundingBox !== null) {
      // Some or all of the element is in view
      elementInViewArea = elementInViewBoundingBox.width * elementInViewBoundingBox.height;
      elementTotalArea = elementBoundingBox.width * elementBoundingBox.height;

      return ((elementInViewArea / elementTotalArea) * 100);
    }

    return 0;
  }

  function _getBoundingBox(element, { w, h } = {}) {
    let { width, height, left, top, right, bottom } = element.getBoundingClientRect();

    if ((width === 0 || height === 0) && w && h) {
      width = w;
      height = h;
      right = left + w;
      bottom = top + h;
    }

    return { width, height, left, top, right, bottom };
  }

  function _getIntersectionOfRects(rects) {
    const bbox = {
      left: rects[0].left,
      right: rects[0].right,
      top: rects[0].top,
      bottom: rects[0].bottom
    };

    for (let i = 1; i < rects.length; ++i) {
      bbox.left = Math.max(bbox.left, rects[i].left);
      bbox.right = Math.min(bbox.right, rects[i].right);

      if (bbox.left >= bbox.right) {
        return null;
      }

      bbox.top = Math.max(bbox.top, rects[i].top);
      bbox.bottom = Math.min(bbox.bottom, rects[i].bottom);

      if (bbox.top >= bbox.bottom) {
        return null;
      }
    }

    bbox.width = bbox.right - bbox.left;
    bbox.height = bbox.bottom - bbox.top;

    return bbox;
  }
}

export const spec = new MarsmediaAdapter();
registerBidder(spec);