prebid/Prebid.js

View on GitHub
modules/visxBidAdapter.js

Summary

Maintainability
F
3 days
Test Coverage
import {deepAccess, logError, parseSizesInput, triggerPixel} from '../src/utils.js';
import {registerBidder} from '../src/adapters/bidderFactory.js';
import {config} from '../src/config.js';
import {BANNER, VIDEO} from '../src/mediaTypes.js';
import {INSTREAM as VIDEO_INSTREAM} from '../src/video.js';
import {getStorageManager} from '../src/storageManager.js';
import {getGptSlotInfoForAdUnitCode} from '../libraries/gptUtils/gptUtils.js';

const BIDDER_CODE = 'visx';
const GVLID = 154;
const BASE_URL = 'https://t.visx.net';
const DEBUG_URL = 'https://t-stage.visx.net';
const ENDPOINT_PATH = '/hb_post';
const TIME_TO_LIVE = 360;
const DEFAULT_CUR = 'EUR';
const ADAPTER_SYNC_PATH = '/push_sync';
const TRACK_TIMEOUT_PATH = '/track/bid_timeout';
const RUNTIME_STATUS_RESPONSE_TIME = 999000;
const LOG_ERROR_MESS = {
  noAuid: 'Bid from response has no auid parameter - ',
  noAdm: 'Bid from response has no adm parameter - ',
  noBid: 'Array of bid objects is empty',
  noImpId: 'Bid from response has no impid parameter - ',
  noPlacementCode: 'Can\'t find in requested bids the bid with auid - ',
  emptyUids: 'Uids should not be empty',
  emptySeatbid: 'Seatbid array from response has an empty item',
  emptyResponse: 'Response is empty',
  hasEmptySeatbidArray: 'Response has empty seatbid array',
  hasNoArrayOfBids: 'Seatbid from response has no array of bid objects - ',
  notAllowedCurrency: 'Currency is not supported - ',
  currencyMismatch: 'Currency from the request is not match currency from the response - ',
  onlyVideoInstream: `Only video ${VIDEO_INSTREAM} supported`,
  videoMissing: 'Bid request videoType property is missing - '
};
const currencyWhiteList = ['EUR', 'USD', 'GBP', 'PLN'];
export const storage = getStorageManager({bidderCode: BIDDER_CODE});
const _bidResponseTimeLogged = [];
export const spec = {
  code: BIDDER_CODE,
  gvlid: GVLID,
  supportedMediaTypes: [BANNER, VIDEO],
  isBidRequestValid: function(bid) {
    if (_isVideoBid(bid)) {
      if (!_isValidVideoBid(bid, true)) {
        // in case if video bid configuration invalid will try to send bid request for banner
        if (!_isBannerBid(bid)) {
          return false;
        }
      }
    }
    return !!bid.params.uid && !isNaN(parseInt(bid.params.uid));
  },
  buildRequests: function(validBidRequests, bidderRequest) {
    const auids = [];
    const bidsMap = {};
    const bids = validBidRequests || [];
    const currency =
      config.getConfig(`currency.bidderCurrencyDefault.${BIDDER_CODE}`) ||
      config.getConfig('currency.adServerCurrency') ||
      DEFAULT_CUR;

    let request;
    let reqId;
    let payloadSchain;
    let payloadUserId;
    let payloadUserEids;
    let timeout;
    let payloadDevice;
    let payloadSite;
    let payloadRegs;
    let payloadContent;

    if (currencyWhiteList.indexOf(currency) === -1) {
      logError(LOG_ERROR_MESS.notAllowedCurrency + currency);
      return;
    }

    const imp = [];

    bids.forEach(bid => {
      reqId = bid.bidderRequestId;

      const impObj = buildImpObject(bid);
      if (impObj) {
        imp.push(impObj);
        bidsMap[bid.bidId] = bid;
      }
      const { params: { uid }, schain, userId, userIdAsEids } = bid;
      if (!payloadSchain && schain) {
        payloadSchain = schain;
      }
      if (!payloadUserEids && userIdAsEids) {
        payloadUserEids = userIdAsEids;
      }

      if (!payloadUserId && userId) {
        payloadUserId = userId;
      }

      auids.push(uid);
    });

    const payload = {};

    if (bidderRequest) {
      timeout = bidderRequest.timeout;

      if (bidderRequest.gdprConsent) {
        if (bidderRequest.gdprConsent.consentString) {
          payload.gdpr_consent = bidderRequest.gdprConsent.consentString;
        }
        payload.gdpr_applies =
            (typeof bidderRequest.gdprConsent.gdprApplies === 'boolean')
              ? Number(bidderRequest.gdprConsent.gdprApplies) : 1;
      }

      const { ortb2 } = bidderRequest;
      const { device, site, regs, content } = ortb2;
      if (device) {
        payloadDevice = device;
      }
      if (site) {
        payloadSite = site;
      }
      if (regs) {
        payloadRegs = regs;
      }
      if (content) {
        payloadContent = content;
      }
    }

    const tmax = timeout;
    const source = {
      ext: {
        wrapperType: 'Prebid_js',
        wrapperVersion: '$prebid.version$',
        ...(payloadSchain && { schain: payloadSchain })
      }
    };

    const vads = _getUserId();
    const user = {
      ext: {
        ...(payloadUserEids && { eids: payloadUserEids }),
        ...(payload.gdpr_consent && { consent: payload.gdpr_consent }),
        ...(vads && { vads })
      }
    };
    if (payloadRegs === undefined) {
      payloadRegs = ('gdpr_applies' in payload) && {
        ext: {
          gdpr: payload.gdpr_applies
        }
      };
    }

    request = {
      id: reqId,
      imp,
      tmax,
      cur: [currency],
      source,
      ...(Object.keys(user.ext).length && { user }),
      ...(payloadRegs && {regs: payloadRegs}),
      ...(payloadDevice && { device: payloadDevice }),
      ...(payloadSite && { site: payloadSite }),
      ...(payloadContent && { content: payloadContent }),
    };

    return {
      method: 'POST',
      url: buildUrl(ENDPOINT_PATH) + '?auids=' + encodeURIComponent(auids.join(',')),
      data: request,
      bidsMap
    };
  },
  interpretResponse: function(serverResponse, bidRequest) {
    serverResponse = serverResponse && serverResponse.body;
    const bidResponses = [];
    const bidsMap = bidRequest.bidsMap;
    const currency = bidRequest.data.cur[0];

    let errorMessage;

    if (!serverResponse) errorMessage = LOG_ERROR_MESS.emptyResponse;
    else if (serverResponse.seatbid && !serverResponse.seatbid.length) {
      errorMessage = LOG_ERROR_MESS.hasEmptySeatbidArray;
    }

    if (!errorMessage && serverResponse.seatbid) {
      serverResponse.seatbid.forEach(respItem => {
        _addBidResponse(_getBidFromResponse(respItem), bidsMap, currency, bidResponses);
      });
    }
    if (errorMessage) logError(errorMessage);
    return bidResponses;
  },
  getUserSyncs: function(syncOptions, serverResponses, gdprConsent) {
    var query = [];
    if (gdprConsent) {
      if (gdprConsent.consentString) {
        query.push('gdpr_consent=' + encodeURIComponent(gdprConsent.consentString));
      }
      query.push('gdpr_applies=' + encodeURIComponent(
        (typeof gdprConsent.gdprApplies === 'boolean')
          ? Number(gdprConsent.gdprApplies) : 1));
    }
    if (syncOptions.iframeEnabled) {
      return [{
        type: 'iframe',
        url: buildUrl(ADAPTER_SYNC_PATH) + '?iframe=1' + (query.length ? '&' + query.join('&') : '')
      }];
    } else if (syncOptions.pixelEnabled) {
      return [{
        type: 'image',
        url: buildUrl(ADAPTER_SYNC_PATH) + (query.length ? '?' + query.join('&') : '')
      }];
    }
  },
  onSetTargeting: function(bid) {
    // Call '/track/pending' with the corresponding bid.requestId
    if (bid.ext && bid.ext.events && bid.ext.events.pending) {
      triggerPixel(bid.ext.events.pending);
    }
  },
  onBidWon: function(bid) {
    // Call '/track/win' with the corresponding bid.requestId
    if (bid.ext && bid.ext.events && bid.ext.events.win) {
      triggerPixel(bid.ext.events.win);
    }
    // Call 'track/runtime' with the corresponding bid.requestId - only once per auction
    if (bid.ext && bid.ext.events && bid.ext.events.runtime && !_bidResponseTimeLogged.includes(bid.auctionId)) {
      _bidResponseTimeLogged.push(bid.auctionId);
      const _roundedTime = _roundResponseTime(bid.timeToRespond, 50);
      triggerPixel(bid.ext.events.runtime.replace('{STATUS_CODE}', RUNTIME_STATUS_RESPONSE_TIME + _roundedTime));
    }
  },
  onTimeout: function(timeoutData) {
    // Call '/track/bid_timeout' with timeout data
    const dataToSend = timeoutData.map(({ params, timeout }) => {
      const data = { timeout };
      if (params) {
        data.params = params.map((item) => {
          return item && item.uid ? { uid: parseInt(item.uid) } : {};
        });
      }
      return data;
    });
    triggerPixel(buildUrl(TRACK_TIMEOUT_PATH) + '//' + JSON.stringify(dataToSend));
  }
};

function buildUrl(path) {
  return (config.getConfig('devMode') ? DEBUG_URL : BASE_URL) + path;
}

function makeBanner(bannerParams) {
  const bannerSizes = bannerParams && bannerParams.sizes;
  if (bannerSizes) {
    const sizes = parseSizesInput(bannerSizes);
    if (sizes.length) {
      const format = sizes.map(size => {
        const [ width, height ] = size.split('x');
        const w = parseInt(width, 10);
        const h = parseInt(height, 10);
        return { w, h };
      });

      return { format };
    }
  }
}

function makeVideo(videoParams = {}) {
  const video = Object.keys(videoParams).filter((param) => param !== 'context' && param !== 'playerSize')
    .reduce((result, param) => {
      result[param] = videoParams[param];
      return result;
    }, { w: deepAccess(videoParams, 'playerSize.0.0'), h: deepAccess(videoParams, 'playerSize.0.1') });

  if (video.w && video.h) {
    return video;
  }
}

function buildImpObject(bid) {
  const { params: { uid }, bidId, mediaTypes, sizes, adUnitCode } = bid;
  const video = mediaTypes && _isVideoBid(bid) && _isValidVideoBid(bid) && makeVideo(mediaTypes.video);
  const banner = makeBanner((mediaTypes && mediaTypes.banner) || (!video && { sizes }));
  const impObject = {
    id: bidId,
    ...(banner && { banner }),
    ...(video && { video }),
    ext: {
      bidder: { uid: parseInt(uid) },
    }
  };

  if (impObject.banner) {
    impObject.ext.bidder.adslotExists = _isAdSlotExists(adUnitCode);
  }

  if (impObject.ext.bidder.uid && (impObject.banner || impObject.video)) {
    return impObject;
  }
}

function _getBidFromResponse(respItem) {
  if (!respItem) {
    logError(LOG_ERROR_MESS.emptySeatbid);
  } else if (!respItem.bid) {
    logError(LOG_ERROR_MESS.hasNoArrayOfBids + JSON.stringify(respItem));
  } else if (!respItem.bid[0]) {
    logError(LOG_ERROR_MESS.noBid);
  }
  return respItem && respItem.bid && respItem.bid[0];
}

function _addBidResponse(serverBid, bidsMap, currency, bidResponses) {
  if (!serverBid) return;
  let errorMessage;
  if (!serverBid.auid) errorMessage = LOG_ERROR_MESS.noAuid + JSON.stringify(serverBid);
  if (!serverBid.impid) errorMessage = LOG_ERROR_MESS.noImpId + JSON.stringify(serverBid);
  if (!serverBid.adm) errorMessage = LOG_ERROR_MESS.noAdm + JSON.stringify(serverBid);
  else {
    const reqCurrency = currency || DEFAULT_CUR;
    const bid = bidsMap[serverBid.impid];
    if (bid) {
      if (serverBid.cur && serverBid.cur !== reqCurrency) {
        errorMessage = LOG_ERROR_MESS.currencyMismatch + reqCurrency + ' - ' + serverBid.cur;
      } else {
        const bidResponse = {
          requestId: bid.bidId,
          cpm: serverBid.price,
          width: serverBid.w,
          height: serverBid.h,
          creativeId: serverBid.auid,
          currency: reqCurrency,
          netRevenue: true,
          ttl: TIME_TO_LIVE,
          dealId: serverBid.dealid,
          meta: {
            advertiserDomains: serverBid.advertiserDomains ? serverBid.advertiserDomains : [],
            mediaType: serverBid.mediaType
          },
        };

        if (serverBid.ext && serverBid.ext.prebid) {
          bidResponse.ext = serverBid.ext.prebid;
          if (serverBid.ext.visx && serverBid.ext.visx.events) {
            const prebidExtEvents = bidResponse.ext.events || {};
            bidResponse.ext.events = Object.assign(prebidExtEvents, serverBid.ext.visx.events);
          }
        }

        const visxTargeting = deepAccess(serverBid, 'ext.prebid.targeting');
        if (visxTargeting) {
          bidResponse.adserverTargeting = visxTargeting;
        }

        if (!_isVideoInstreamBid(bid)) {
          bidResponse.ad = serverBid.adm;
        } else {
          bidResponse.vastXml = serverBid.adm;
          bidResponse.mediaType = 'video';
        }

        bidResponses.push(bidResponse);
      }
    } else {
      errorMessage = LOG_ERROR_MESS.noPlacementCode + serverBid.auid;
    }
  }
  if (errorMessage) {
    logError(errorMessage);
  }
}

function _isVideoBid(bid) {
  return bid.mediaType === VIDEO || deepAccess(bid, 'mediaTypes.video');
}

function _isVideoInstreamBid(bid) {
  return _isVideoBid(bid) && deepAccess(bid, 'mediaTypes.video', {}).context === VIDEO_INSTREAM;
}

function _isBannerBid(bid) {
  return bid.mediaType === BANNER || deepAccess(bid, 'mediaTypes.banner');
}

function _isValidVideoBid(bid, logErrors = false) {
  let result = true;
  const videoMediaType = deepAccess(bid, 'mediaTypes.video');
  if (!_isVideoInstreamBid(bid)) {
    if (logErrors) {
      logError(LOG_ERROR_MESS.onlyVideoInstream);
    }
    result = false;
  }
  if (!(videoMediaType.playerSize && parseSizesInput(deepAccess(videoMediaType, 'playerSize', [])))) {
    if (logErrors) {
      logError(LOG_ERROR_MESS.videoMissing + 'playerSize');
    }
    result = false;
  }
  return result;
}

function _isAdSlotExists(adUnitCode) {
  if (document.getElementById(adUnitCode)) {
    return true;
  }

  const gptAdSlot = getGptSlotInfoForAdUnitCode(adUnitCode);
  if (gptAdSlot.divId && document.getElementById(gptAdSlot.divId)) {
    return true;
  }

  return false;
}

// Generate user id (25 chars) with NanoID
// https://github.com/ai/nanoid/
function _generateUserId() {
  for (
    var t = 'useandom-26T198340PX75pxJACKVERYMINDBUSHWOLF_GQZbfghjklqvwyzrict',
      e = new Date().getTime() % 1073741824,
      i = '',
      o = 0;
    o < 5;
    o++
  ) {
    i += t[e % 64];
    e = Math.floor(e / 64);
  }
  for (o = 20; o--;) i += t[(64 * Math.random()) | 0];
  return i;
}

function _getUserId() {
  const USER_ID_KEY = '__vads';
  let vads;

  if (storage.cookiesAreEnabled()) {
    vads = storage.getCookie(USER_ID_KEY);
  } else if (storage.localStorageIsEnabled()) {
    vads = storage.getDataFromLocalStorage(USER_ID_KEY);
  }

  if (vads && vads.length) {
    return vads;
  }

  vads = _generateUserId();
  if (storage.cookiesAreEnabled()) {
    const expires = new Date(Date.now() + 2592e6).toUTCString();
    storage.setCookie(USER_ID_KEY, vads, expires);
    return vads;
  } else if (storage.localStorageIsEnabled()) {
    storage.setDataInLocalStorage(USER_ID_KEY, vads);
    return vads;
  }

  return null;
}

function _roundResponseTime(time, timeRange) {
  if (time <= 0) {
    return 0; // Special code for scriptLoadTime of 0 ms or less
  } else if (time > 5000) {
    return 100; // Constant code for scriptLoadTime greater than 5000 ms
  } else {
    const roundedValue = Math.floor((time - 1) / timeRange) + 1;
    return roundedValue;
  }
}

registerBidder(spec);