prebid/Prebid.js

View on GitHub
modules/underdogmediaBidAdapter.js

Summary

Maintainability
F
5 days
Test Coverage
import {
  deepAccess,
  flatten,
  getWindowSelf,
  getWindowTop,
  isGptPubadsDefined,
  logInfo,
  logMessage,
  logWarn,
  parseSizesInput
} from '../src/utils.js';
import {config} from '../src/config.js';
import {registerBidder} from '../src/adapters/bidderFactory.js';
import {isSlotMatchingAdUnitCode} from '../libraries/gptUtils/gptUtils.js';

const BIDDER_CODE = 'underdogmedia';
const UDM_ADAPTER_VERSION = '7.30V';
const UDM_VENDOR_ID = '159';
const prebidVersion = '$prebid.version$';
const NON_MEASURABLE = -1;
const PRODUCT = {
  standard: 1,
  sticky: 2
}

let USER_SYNCED = false;

logMessage(`Initializing UDM Adapter. PBJS Version: ${prebidVersion} with adapter version: ${UDM_ADAPTER_VERSION}  Updated 2023 01 26`);

// helper function for testing user syncs
export function resetUserSync() {
  USER_SYNCED = false;
}

export const spec = {
  NON_MEASURABLE,
  code: BIDDER_CODE,
  bidParams: [],

  isBidRequestValid: function (bid) {
    if (!bid.params) {
      logWarn('[Underdog Media] bid params are missing')
      return false;
    }

    if (!bid.params.siteId) {
      logWarn('[Underdog Media] siteId is missing')
      return false;
    }

    if (bid.params.productId) {
      if (!PRODUCT[bid.params.productId]) {
        logWarn('[Underdog Media] invalid productId')
        return false;
      }
    }

    const bidSizes = bid.mediaTypes && bid.mediaTypes.banner && bid.mediaTypes.banner.sizes ? bid.mediaTypes.banner.sizes : bid.sizes;
    if (!bidSizes || bidSizes.length < 1) {
      logWarn('[Underdog Media] bid sizes are missing')
      return false;
    }

    return true;
  },

  buildRequests: function (validBidRequests, bidderRequest) {
    var sizes = [];
    var siteId = 0;

    let data = {
      dt: 10,
      gdpr: {},
      pbTimeout: +config.getConfig('bidderTimeout') || 3001, // KP: convert to number and if NaN we default to 3001. Particular value to let us know that there was a problem in converting pbTimeout
      pbjsVersion: prebidVersion,
      placements: [],
      ref: deepAccess(bidderRequest, 'refererInfo.page') ? bidderRequest.refererInfo.page : undefined,
      usp: {},
      userIds: {
        '33acrossId': deepAccess(validBidRequests[0], 'userId.33acrossId.envelope') ? validBidRequests[0].userId['33acrossId'].envelope : undefined,
        pubcid: deepAccess(validBidRequests[0], 'crumbs.pubcid') ? validBidRequests[0].crumbs.pubcid : undefined,
        unifiedId: deepAccess(validBidRequests[0], 'userId.tdid') ? validBidRequests[0].userId.tdid : undefined
      },
      version: UDM_ADAPTER_VERSION
    }

    validBidRequests.forEach(bidParam => {
      let placementObject = {}
      let bidParamSizes = bidParam.mediaTypes && bidParam.mediaTypes.banner && bidParam.mediaTypes.banner.sizes ? bidParam.mediaTypes.banner.sizes : bidParam.sizes;
      sizes = flatten(sizes, parseSizesInput(bidParamSizes));
      siteId = +bidParam.params.siteId;
      let adUnitCode = bidParam.adUnitCode
      let element = _getAdSlotHTMLElement(adUnitCode)
      let minSize = _getMinSize(bidParamSizes)

      placementObject.sizes = parseSizesInput(bidParamSizes)
      placementObject.adUnitCode = adUnitCode
      placementObject.productId = PRODUCT[bidParam.params.productId] || PRODUCT.standard
      if (deepAccess(bidParam, 'params.productId')) {
        if (bidParam.params.productId === 'standard') {
          placementObject.productId = 1
        } else if (bidParam.params.productId === 'adhesion') {
          placementObject.productId = 2
        }
      } else {
        placementObject.productId = 1
      }
      placementObject.gpid = deepAccess(bidParam, 'ortb2Imp.ext.gpid') ? bidParam.ortb2Imp.ext.gpid : undefined

      if (_isViewabilityMeasurable(element)) {
        const minSizeObj = {
          w: minSize[0],
          h: minSize[1]
        }
        let viewPercentage = Math.round(_getViewability(element, getWindowTop(), minSizeObj))
        placementObject.viewability = viewPercentage
      } else {
        placementObject.viewability = NON_MEASURABLE
      }

      data.placements.push(placementObject)
    });

    data.sid = siteId

    if (bidderRequest && bidderRequest.gdprConsent) {
      if (typeof bidderRequest.gdprConsent.gdprApplies !== 'undefined') {
        data.gdpr.gdprApplies = !!(bidderRequest.gdprConsent.gdprApplies);
      }
      if (bidderRequest.gdprConsent.vendorData && bidderRequest.gdprConsent.vendorData.vendorConsents &&
        typeof bidderRequest.gdprConsent.vendorData.vendorConsents[UDM_VENDOR_ID] !== 'undefined') {
        data.gdpr.consentGiven = !!(bidderRequest.gdprConsent.vendorData.vendorConsents[UDM_VENDOR_ID]);
      }
      if (typeof bidderRequest.gdprConsent.consentString !== 'undefined') {
        data.gdpr.consentData = bidderRequest.gdprConsent.consentString;
      }
    }

    if (bidderRequest.uspConsent) {
      data.usp.uspConsent = bidderRequest.uspConsent;
    }

    if (!data.gdpr || !data.gdpr.gdprApplies || data.gdpr.consentGiven) {
      return {
        method: 'POST',
        url: `https://udmserve.net/udm/img.fetch?sid=${siteId}`,
        data: data,
        bidParams: validBidRequests
      };
    }
  },

  getUserSyncs: function (syncOptions, serverResponses) {
    if (!USER_SYNCED && serverResponses.length > 0 && serverResponses[0].body && serverResponses[0].body.userSyncs && serverResponses[0].body.userSyncs.length > 0) {
      USER_SYNCED = true;
      const userSyncs = serverResponses[0].body.userSyncs;
      const syncs = userSyncs.filter(sync => {
        const {
          type
        } = sync;
        if (syncOptions.iframeEnabled && type === 'iframe') {
          return true
        }
        if (syncOptions.pixelEnabled && type === 'image') {
          return true
        }
      })
      return syncs;
    }
  },

  interpretResponse: function (serverResponse, bidRequest) {
    const bidResponses = [];
    const mids = serverResponse.body.mids
    mids.forEach(mid => {
      const bidParam = bidRequest.bidParams.find((bidParam) => {
        if (mid.ad_unit_code === bidParam.adUnitCode) {
          return true
        }
      })

      if (!bidParam) {
        return
      }

      const bidResponse = {
        requestId: bidParam.bidId,
        cpm: parseFloat(mid.cpm),
        width: mid.width,
        height: mid.height,
        ad: mid.ad_code_html,
        creativeId: mid.mid,
        currency: 'USD',
        netRevenue: false,
        ttl: mid.ttl || 300,
        meta: {
          advertiserDomains: mid.advertiser_domains || []
        }
      };

      if (bidResponse.cpm <= 0) {
        return;
      }
      if (bidResponse.ad.length <= 0) {
        return;
      }

      bidResponse.ad += makeNotification(bidResponse, mid, bidParam);

      bidResponses.push(bidResponse);
    });

    return bidResponses;
  },
};

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

function _getAdSlotHTMLElement(adUnitCode) {
  return document.getElementById(adUnitCode) ||
    document.getElementById(_mapAdUnitPathToElementId(adUnitCode));
}

function _mapAdUnitPathToElementId(adUnitCode) {
  if (isGptPubadsDefined()) {
    // eslint-disable-next-line no-undef
    const adSlots = googletag.pubads().getSlots();
    const isMatchingAdSlot = isSlotMatchingAdUnitCode(adUnitCode);

    for (let i = 0; i < adSlots.length; i++) {
      if (isMatchingAdSlot(adSlots[i])) {
        const id = adSlots[i].getSlotElementId();

        logInfo(`[Underdogmedia Adapter] Map ad unit path to HTML element id: '${adUnitCode}' -> ${id}`);

        return id;
      }
    }
  }

  logWarn(`[Underdogmedia Adapter] Unable to locate element for ad unit code: '${adUnitCode}'`);

  return null;
}

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
  });

  // Obtain the intersection of the element and the viewport
  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);
  }

  // No overlap between element and the viewport; therefore, the element
  // lies completely out of view
  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;
}

function makeNotification(bid, mid, bidParam) {
  let url = mid.notification_url;

  const versionIndex = url.indexOf(';version=')
  if (versionIndex + 1) {
    url = url.substring(0, versionIndex)
  }

  url += `;version=${UDM_ADAPTER_VERSION}`;
  url += ';cb=' + Math.random();
  url += ';qqq=' + (1 / bid.cpm);
  url += ';hbt=' + config.getConfig('bidderTimeout');
  url += ';style=adapter';
  url += ';vis=' + encodeURIComponent(document.visibilityState);

  url += ';traffic_info=' + encodeURIComponent(JSON.stringify(getUrlVars()));
  if (bidParam.params.subId) {
    url += ';subid=' + encodeURIComponent(bidParam.params.subId);
  }
  return '<script async src="' + url + '"></script>';
}

function getUrlVars() {
  var vars = {};
  var hash;
  var hashes = window.location.href.slice(window.location.href.indexOf('?') + 1).split('&');
  for (var i = 0; i < hashes.length; i++) {
    hash = hashes[i].split('=');
    if (hash[0].match(/^utm_/)) {
      vars[hash[0]] = hash[1].substr(0, 150);
    }
  }
  return vars;
}

registerBidder(spec);