prebid/Prebid.js

View on GitHub
modules/eplanningBidAdapter.js

Summary

Maintainability
F
3 days
Test Coverage
import {getWindowSelf, isEmpty, parseSizesInput, isGptPubadsDefined} from '../src/utils.js';
import {getGlobal} from '../src/prebidGlobal.js';
import {registerBidder} from '../src/adapters/bidderFactory.js';
import {getStorageManager} from '../src/storageManager.js';
import {BANNER, VIDEO} from '../src/mediaTypes.js';
import {isSlotMatchingAdUnitCode} from '../libraries/gptUtils/gptUtils.js';

const BIDDER_CODE = 'eplanning';
export const storage = getStorageManager({bidderCode: BIDDER_CODE});
const rnd = Math.random();
const DEFAULT_SV = 'pbjs.e-planning.net';
const DEFAULT_ISV = 'i.e-planning.net';
const PARAMS = ['ci', 'sv', 't', 'ml', 'sn'];
const DOLLAR_CODE = 'USD';
const NET_REVENUE = true;
const TTL = 120;
const NULL_SIZE = '1x1';
const FILE = 'file';
const STORAGE_RENDER_PREFIX = 'pbsr_';
const STORAGE_VIEW_PREFIX = 'pbvi_';
const mobileUserAgent = isMobileUserAgent();
const PRIORITY_ORDER_FOR_MOBILE_SIZES_ASC = ['1x1', '300x50', '320x50', '300x250'];
const PRIORITY_ORDER_FOR_DESKTOP_SIZES_ASC = ['1x1', '970x90', '970x250', '160x600', '300x600', '728x90', '300x250'];
const VAST_INSTREAM = 1;
const VAST_OUTSTREAM = 2;
const VAST_VERSION_DEFAULT = 3;
const DEFAULT_SIZE_VAST = '640x480';
const MAX_LEN_URL = 255;

export const spec = {
  code: BIDDER_CODE,
  supportedMediaTypes: [BANNER, VIDEO],

  isBidRequestValid: function(bid) {
    return Boolean(bid.params.ci) || Boolean(bid.params.t);
  },

  buildRequests: function(bidRequests, bidderRequest) {
    const method = 'GET';
    const dfpClientId = '1';
    const sec = 'ROS';
    let url;
    let params;
    const urlConfig = getUrlConfig(bidRequests);
    const pcrs = getCharset();
    const spaces = getSpaces(bidRequests, urlConfig.ml);
    // TODO: do the fallbacks make sense here?
    const pageUrl = bidderRequest.refererInfo.page || bidderRequest.refererInfo.topmostLocation;
    const domain = bidderRequest.refererInfo.domain || window.location.host;
    if (urlConfig.t) {
      url = 'https://' + urlConfig.isv + '/layers/t_pbjs_2.json';
      params = {};
    } else {
      url = 'https://' + (urlConfig.sv || DEFAULT_SV) + '/pbjs/1/' + urlConfig.ci + '/' + dfpClientId + '/' + domain + '/' + sec;
      // TODO: does the fallback make sense here?
      const referrerUrl = bidderRequest.refererInfo.ref || bidderRequest.refererInfo.topmostLocation;

      if (storage.hasLocalStorage()) {
        registerViewabilityAllBids(bidRequests);
      }

      params = {
        rnd: rnd,
        e: spaces.str,
        ur: cutUrl(pageUrl || FILE),
        pbv: '$prebid.version$',
        ncb: '1',
        vs: spaces.vs
      };
      if (pcrs) {
        params.crs = pcrs;
      }

      if (referrerUrl) {
        params.fr = cutUrl(referrerUrl);
      }

      if (bidderRequest && bidderRequest.gdprConsent) {
        if (typeof bidderRequest.gdprConsent.gdprApplies !== 'undefined') {
          params.gdpr = bidderRequest.gdprConsent.gdprApplies ? '1' : '0';
          if (typeof bidderRequest.gdprConsent.consentString !== 'undefined') {
            params.gdprcs = bidderRequest.gdprConsent.consentString;
          }
        }
      }

      if (bidderRequest && bidderRequest.uspConsent) {
        params.ccpa = bidderRequest.uspConsent;
      }

      if ((getGlobal()).getUserIds && typeof (getGlobal()).getUserIds === 'function') {
        const userIds = (getGlobal()).getUserIds();
        for (var id in userIds) {
          params['e_' + id] = (typeof userIds[id] === 'object') ? encodeURIComponent(JSON.stringify(userIds[id])) : encodeURIComponent(userIds[id]);
        }
      }
      if (spaces.impType) {
        params.vctx = spaces.impType & VAST_INSTREAM ? VAST_INSTREAM : VAST_OUTSTREAM;
        params.vv = VAST_VERSION_DEFAULT;
      }
    }

    return {
      method: method,
      url: url,
      data: params,
      adUnitToBidId: spaces.map,
    };
  },
  interpretResponse: function(serverResponse, request) {
    const response = serverResponse.body;
    let bidResponses = [];

    if (response && !isEmpty(response.sp)) {
      response.sp.forEach(space => {
        if (!isEmpty(space.a)) {
          space.a.forEach(ad => {
            const bidResponse = {
              requestId: request.adUnitToBidId[space.k],
              cpm: ad.pr,
              width: ad.w,
              height: ad.h,
              ttl: TTL,
              creativeId: ad.crid,
              netRevenue: NET_REVENUE,
              currency: DOLLAR_CODE,
            };
            if (ad.adom) {
              bidResponse.meta = {
                advertiserDomains: ad.adom
              };
            }
            if (request && request.data && request.data.vv) {
              bidResponse.vastXml = ad.adm;
              bidResponse.mediaType = VIDEO;
            } else {
              bidResponse.ad = ad.adm;
            }

            bidResponses.push(bidResponse);
          });
        }
      });
    }

    return bidResponses;
  },
  getUserSyncs: function(syncOptions, serverResponses) {
    const syncs = [];
    const response = !isEmpty(serverResponses) && serverResponses[0].body;

    if (response && !isEmpty(response.cs)) {
      const responseSyncs = response.cs;
      responseSyncs.forEach(sync => {
        if (typeof sync === 'string' && syncOptions.pixelEnabled) {
          syncs.push({
            type: 'image',
            url: sync,
          });
        } else if (typeof sync === 'object' && sync.ifr && syncOptions.iframeEnabled) {
          syncs.push({
            type: 'iframe',
            url: sync.u,
          })
        }
      });
    }

    return syncs;
  },
};

function getUserAgent() {
  return window.navigator.userAgent;
}
function getInnerWidth() {
  return getWindowSelf().innerWidth;
}
function isMobileUserAgent() {
  return getUserAgent().match(/(mobile)|(ip(hone|ad))|(android)|(blackberry)|(nokia)|(phone)|(opera\smini)/i);
}
function isMobileDevice() {
  return (getInnerWidth() <= 1024) || window.orientation || mobileUserAgent;
}
function getUrlConfig(bidRequests) {
  if (isTestRequest(bidRequests)) {
    return getTestConfig(bidRequests.filter(br => br.params.t));
  }

  let config = {};
  bidRequests.forEach(bid => {
    PARAMS.forEach(param => {
      if (bid.params[param] && !config[param]) {
        config[param] = bid.params[param];
      }
    });
  });

  return config;
}
function isTestRequest(bidRequests) {
  for (let i = 0; i < bidRequests.length; i++) {
    if (bidRequests[i].params.t) {
      return true;
    }
  }
  return false;
}
function getTestConfig(bidRequests) {
  let isv;
  bidRequests.forEach(br => isv = isv || br.params.isv);
  return {
    t: true,
    isv: (isv || DEFAULT_ISV)
  };
}

function compareSizesByPriority(size1, size2) {
  var priorityOrderForSizesAsc = isMobileDevice() ? PRIORITY_ORDER_FOR_MOBILE_SIZES_ASC : PRIORITY_ORDER_FOR_DESKTOP_SIZES_ASC;
  var index1 = priorityOrderForSizesAsc.indexOf(size1);
  var index2 = priorityOrderForSizesAsc.indexOf(size2);
  if (index1 > -1) {
    if (index2 > -1) {
      return (index1 < index2) ? 1 : -1;
    } else {
      return -1;
    }
  } else {
    return (index2 > -1) ? 1 : 0;
  }
}

function getSizesSortedByPriority(sizes) {
  return parseSizesInput(sizes).sort(compareSizesByPriority);
}

function getSize(bid, first) {
  var arraySizes = bid.sizes && bid.sizes.length ? getSizesSortedByPriority(bid.sizes) : [];
  if (arraySizes.length) {
    return first ? arraySizes[0] : arraySizes.join(',');
  } else {
    return NULL_SIZE;
  }
}

function getSpacesStruct(bids) {
  let e = {};
  bids.forEach(bid => {
    let size = getSize(bid, true);
    e[size] = e[size] ? e[size] : [];
    e[size].push(bid);
  });

  return e;
}

function getFirstSizeVast(sizes) {
  if (sizes == undefined || !Array.isArray(sizes)) {
    return undefined;
  }

  let size = Array.isArray(sizes[0]) ? sizes[0] : sizes;

  return (Array.isArray(size) && size.length == 2) ? size : undefined;
}

function cleanName(name) {
  return name.replace(/_|\.|-|\//g, '').replace(/\)\(|\(|\)|:/g, '_').replace(/^_+|_+$/g, '');
}

function getFloorStr(bid) {
  if (typeof bid.getFloor === 'function') {
    let bidFloor = bid.getFloor({
      currency: DOLLAR_CODE,
      mediaType: '*',
      size: '*'
    });

    if (bidFloor.floor) {
      return '|' + encodeURIComponent(bidFloor.floor);
    }
  }
  return '';
}

function getSpaces(bidRequests, ml) {
  let impType = bidRequests.reduce((previousBits, bid) => (bid.mediaTypes && bid.mediaTypes[VIDEO]) ? (bid.mediaTypes[VIDEO].context == 'outstream' ? (previousBits | 2) : (previousBits | 1)) : previousBits, 0);
  // Only one type of auction is supported at a time
  if (impType) {
    bidRequests = bidRequests.filter((bid) => bid.mediaTypes && bid.mediaTypes[VIDEO] && (impType & VAST_INSTREAM ? (!bid.mediaTypes[VIDEO].context || bid.mediaTypes[VIDEO].context == 'instream') : (bid.mediaTypes[VIDEO].context == 'outstream')));
  }

  let spacesStruct = getSpacesStruct(bidRequests);
  let es = {str: '', vs: '', map: {}, impType: impType};
  es.str = Object.keys(spacesStruct).map(size => spacesStruct[size].map((bid, i) => {
    es.vs += getVs(bid);

    let name;

    if (impType) {
      let firstSize = getFirstSizeVast(bid.mediaTypes[VIDEO].playerSize);
      let sizeVast = firstSize ? firstSize.join('x') : DEFAULT_SIZE_VAST;
      name = 'video_' + sizeVast + '_' + i;
      es.map[name] = bid.bidId;
      return name + ':' + sizeVast + ';1' + getFloorStr(bid);
    }

    if (ml) {
      name = cleanName(bid.adUnitCode);
    } else {
      name = (bid.params && bid.params.sn) || (getSize(bid, true) + '_' + i);
    }

    es.map[name] = bid.bidId;
    return name + ':' + getSize(bid) + getFloorStr(bid);
  }).join('+')).join('+');
  return es;
}

function getVs(bid) {
  let s;
  let vs = '';
  if (storage.hasLocalStorage()) {
    s = getViewabilityData(bid);
    vs += s.render >= 4 ? s.ratio.toString(16) : 'F';
  } else {
    vs += 'F';
  }
  return vs;
}

function getViewabilityData(bid) {
  let r = storage.getDataFromLocalStorage(STORAGE_RENDER_PREFIX + bid.adUnitCode) || 0;
  let v = storage.getDataFromLocalStorage(STORAGE_VIEW_PREFIX + bid.adUnitCode) || 0;
  let ratio = r > 0 ? (v / r) : 0;
  return {
    render: r,
    ratio: window.parseInt(ratio * 10, 10)
  };
}

function getCharset() {
  try {
    return window.top.document.charset || window.top.document.characterSet;
  } catch (e) {
    return document.charset || document.characterSet;
  }
}

function waitForElementsPresent(elements) {
  const observer = new MutationObserver(function (mutationList, observer) {
    let index;
    let adView;
    if (mutationList && Array.isArray(mutationList)) {
      mutationList.forEach(mr => {
        if (mr && mr.addedNodes && Array.isArray(mr.addedNodes)) {
          mr.addedNodes.forEach(ad => {
            index = elements.indexOf(ad.id);
            adView = ad;
            if (index < 0) {
              elements.forEach(code => {
                let div = _getAdSlotHTMLElement(code);
                if (div && div.contains(ad) && div.getBoundingClientRect().width > 0) {
                  index = elements.indexOf(div.id);
                  adView = div;
                }
              });
            }
            if (index >= 0) {
              registerViewability(adView, elements[index]);
              elements.splice(index, 1);
              if (!elements.length) {
                observer.disconnect();
              }
            }
          });
        }
      });
    }
  });
  document.addEventListener('DOMContentLoaded', function (event) {
    var config = {
      childList: true,
      subtree: true,
      characterData: true
    };
    observer.observe(document.body, config);
  });
}

function registerViewability(div, name) {
  visibilityHandler({
    name: name,
    div: div
  });
}

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();
        return id;
      }
    }
  }

  return null;
}

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

function registerViewabilityAllBids(bids) {
  let elementsNotPresent = [];
  bids.forEach(bid => {
    let div = _getAdSlotHTMLElement(bid.adUnitCode);
    if (div) {
      registerViewability(div, bid.adUnitCode);
    } else {
      elementsNotPresent.push(bid.adUnitCode);
    }
  });
  if (elementsNotPresent.length) {
    waitForElementsPresent(elementsNotPresent);
  }
}

function getViewabilityTracker() {
  let TIME_PARTITIONS = 5;
  let VIEWABILITY_TIME = 1000;
  let VIEWABILITY_MIN_RATIO = 0.5;
  let publicApi;
  let observer;
  let visibilityAds = {};

  function intersectionCallback(entries) {
    entries.forEach(function(entry) {
      var adBox = entry.target;
      if (entry.isIntersecting) {
        if (entry.intersectionRatio >= VIEWABILITY_MIN_RATIO && entry.boundingClientRect && entry.boundingClientRect.height > 0 && entry.boundingClientRect.width > 0) {
          visibilityAds[adBox.id] = true;
        }
      } else {
        visibilityAds[adBox.id] = false;
      }
    });
  }

  function observedElementIsVisible(element) {
    return visibilityAds[element.id] && document.visibilityState && document.visibilityState === 'visible';
  }

  function defineObserver() {
    if (!observer) {
      var observerConfig = {
        root: null,
        rootMargin: '0px',
        threshold: [VIEWABILITY_MIN_RATIO]
      };
      observer = new IntersectionObserver(intersectionCallback.bind(this), observerConfig);
    }
  }
  function processIntervalVisibilityStatus(elapsedVisibleIntervals, element, callback) {
    let visibleIntervals = observedElementIsVisible(element) ? (elapsedVisibleIntervals + 1) : 0;
    if (visibleIntervals === TIME_PARTITIONS) {
      stopObserveViewability(element)
      callback();
    } else {
      setTimeout(processIntervalVisibilityStatus.bind(this, visibleIntervals, element, callback), VIEWABILITY_TIME / TIME_PARTITIONS);
    }
  }

  function stopObserveViewability(element) {
    delete visibilityAds[element.id];
    observer.unobserve(element);
  }

  function observeAds(element) {
    observer.observe(element);
  }

  function initAndVerifyVisibility(element, callback) {
    if (element) {
      defineObserver();
      observeAds(element);
      processIntervalVisibilityStatus(0, element, callback);
    }
  }

  publicApi = {
    onView: initAndVerifyVisibility.bind(this)
  };

  return publicApi;
};

function visibilityHandler(obj) {
  if (obj.div) {
    registerAuction(STORAGE_RENDER_PREFIX + obj.name);
    getViewabilityTracker().onView(obj.div, registerAuction.bind(undefined, STORAGE_VIEW_PREFIX + obj.name));
  }
}

function cutUrl (url) {
  if (url.length > MAX_LEN_URL) {
    url = url.split('?')[0];
    if (url.length > MAX_LEN_URL) {
      url = url.slice(0, MAX_LEN_URL);
    }
  }

  return url;
}

function registerAuction(storageID) {
  let value;
  try {
    value = storage.getDataFromLocalStorage(storageID);
    value = value ? window.parseInt(value, 10) + 1 : 1;
    storage.setDataInLocalStorage(storageID, value);
  } catch (exc) {
    return false;
  }

  return true;
}

registerBidder(spec);