prebid/Prebid.js

View on GitHub
modules/jwplayerBidAdapter.js

Summary

Maintainability
F
3 days
Test Coverage
import { registerBidder } from '../src/adapters/bidderFactory.js';
import { VIDEO } from '../src/mediaTypes.js';
import { isArray, isFn, deepAccess, deepSetValue, getDNT, logError, logWarn } from '../src/utils.js';
import { config } from '../src/config.js';
import { hasPurpose1Consent } from '../src/utils/gdpr.js';

const BIDDER_CODE = 'jwplayer';
const BASE_URL = 'https://vpb-server.jwplayer.com/';
const AUCTION_URL = BASE_URL + 'openrtb2/auction';
const USER_SYNC_URL = BASE_URL + 'setuid';
const GVLID = 1046;
const SUPPORTED_AD_TYPES = [VIDEO];

const VIDEO_ORTB_PARAMS = [
  'pos',
  'w',
  'h',
  'playbackend',
  'mimes',
  'minduration',
  'maxduration',
  'protocols',
  'startdelay',
  'placement',
  'plcmt',
  'skip',
  'skipafter',
  'minbitrate',
  'maxbitrate',
  'delivery',
  'playbackmethod',
  'api',
  'linearity'
];

function getBidAdapter() {
  function isBidRequestValid(bid) {
    const params = bid && bid.params;
    if (!params) {
      return false;
    }

    return !!params.placementId && !!params.publisherId && !!params.siteId;
  }

  function buildRequests(bidRequests, bidderRequest) {
    if (!bidRequests) {
      return;
    }

    if (!hasContentUrl(bidderRequest.ortb2)) {
      logError(`${BIDDER_CODE}: cannot bid without a valid Content URL. Please populate ortb2.site.content.url`);
      return;
    }

    const warnings = getWarnings(bidderRequest);
    warnings.forEach(warning => {
      logWarn(`${BIDDER_CODE}: ${warning}`);
    });

    return bidRequests.map(bidRequest => {
      const payload = buildRequest(bidRequest, bidderRequest);

      return {
        method: 'POST',
        url: AUCTION_URL,
        data: payload
      }
    });
  }

  function interpretResponse(serverResponse) {
    const outgoingBidResponses = [];
    const serverResponseBody = serverResponse.body;

    logResponseWarnings(serverResponseBody);

    const seatBids = serverResponseBody && serverResponseBody.seatbid;
    if (!isArray(seatBids)) {
      return outgoingBidResponses;
    }

    const cur = serverResponseBody.cur;

    seatBids.forEach(seatBid => {
      seatBid.bid.forEach(bid => {
        const bidResponse = {
          requestId: serverResponseBody.id,
          cpm: bid.price,
          currency: cur,
          width: bid.w,
          height: bid.h,
          ad: bid.adm,
          vastXml: bid.adm,
          ttl: bid.ttl || 3600,
          netRevenue: false,
          creativeId: bid.adid,
          dealId: bid.dealid,
          meta: {
            advertiserDomains: bid.adomain,
            mediaType: VIDEO,
            primaryCatId: bid.cat,
          }
        };

        outgoingBidResponses.push(bidResponse);
      });
    });

    return outgoingBidResponses;
  }

  function getUserSyncs(syncOptions, serverResponses, gdprConsent, uspConsent) {
    if (!hasPurpose1Consent(gdprConsent)) {
      return [];
    }

    const userSyncs = [];
    const consentQueryParams = getUserSyncConsentQueryParams(gdprConsent);
    const url = `https://ib.adnxs.com/getuid?${USER_SYNC_URL}?bidder=jwplayer&uid=$UID&f=i` + consentQueryParams

    if (syncOptions.iframeEnabled) {
      userSyncs.push({
        type: 'iframe',
        url
      });
    }

    if (syncOptions.pixelEnabled) {
      userSyncs.push({
        type: 'image',
        url
      });
    }

    return userSyncs;
  }

  return {
    code: BIDDER_CODE,
    gvlid: GVLID,
    supportedMediaTypes: SUPPORTED_AD_TYPES,
    isBidRequestValid,
    buildRequests,
    interpretResponse,
    getUserSyncs
  }

  function getUserSyncConsentQueryParams(gdprConsent) {
    if (!gdprConsent) {
      return '';
    }

    const consentString = gdprConsent.consentString;
    if (!consentString) {
      return '';
    }

    let gdpr = 0;
    const gdprApplies = gdprConsent.gdprApplies;
    if (typeof gdprApplies === 'boolean') {
      gdpr = Number(gdprApplies)
    }

    return `&gdpr=${gdpr}&gdpr_consent=${consentString}`;
  }

  function buildRequest(bidRequest, bidderRequest) {
    const openrtbRequest = {
      id: bidRequest.bidId,
      imp: getRequestImpressions(bidRequest, bidderRequest),
      site: getRequestSite(bidRequest, bidderRequest),
      device: getRequestDevice(bidderRequest.ortb2),
      user: getRequestUser(bidderRequest.ortb2),
    };

    // GDPR Consent Params
    if (bidderRequest.gdprConsent) {
      deepSetValue(openrtbRequest, 'user.ext.consent', bidderRequest.gdprConsent.consentString);
      deepSetValue(openrtbRequest, 'regs.ext.gdpr', (bidderRequest.gdprConsent.gdprApplies ? 1 : 0));
    }

    // CCPA
    if (bidderRequest.uspConsent) {
      deepSetValue(openrtbRequest, 'regs.ext.us_privacy', bidderRequest.uspConsent);
    }

    if (bidRequest.schain) {
      deepSetValue(openrtbRequest, 'source.schain', bidRequest.schain);
    }

    openrtbRequest.tmax = bidderRequest.timeout || 200;

    return JSON.stringify(openrtbRequest);
  }

  function getRequestImpressions(bidRequest) {
    const impressionObject = {
      id: bidRequest.adUnitCode,
    };

    impressionObject.video = getImpressionVideo(bidRequest);

    const bidFloorData = getBidFloorData(bidRequest);
    if (bidFloorData) {
      impressionObject.bidfloor = bidFloorData.floor;
      impressionObject.bidfloorcur = bidFloorData.currency;
    }

    impressionObject.ext = getImpressionExtension(bidRequest);

    return [impressionObject];
  }

  function getImpressionVideo(bidRequest) {
    const videoParams = deepAccess(bidRequest, 'mediaTypes.video', {});

    const video = {};

    VIDEO_ORTB_PARAMS.forEach((param) => {
      if (videoParams.hasOwnProperty(param)) {
        video[param] = videoParams[param];
      }
    });

    setPlayerSize(video, videoParams);

    if (!videoParams.plcmt) {
      logWarn(`${BIDDER_CODE}: Please set a value to mediaTypes.video.plcmt`);
    }

    return video;
  }

  function getImpressionExtension(bidRequest) {
    return {
      prebid: {
        bidder: {
          jwplayer: {
            placementId: bidRequest.params.placementId
          }
        }
      }
    };
  }

  function setPlayerSize(videoImp, videoParams) {
    if (videoImp.w !== undefined && videoImp.h !== undefined) {
      return;
    }

    const playerSize = getNormalizedPlayerSize(videoParams.playerSize);
    if (!playerSize.length) {
      logWarn(logWarn(`${BIDDER_CODE}: Video size has not been set. Please set values in video.h and video.w`));
      return;
    }

    if (videoImp.w === undefined) {
      videoImp.w = playerSize[0];
    }

    if (videoImp.h === undefined) {
      videoImp.h = playerSize[1];
    }
  }

  function getNormalizedPlayerSize(playerSize) {
    if (!Array.isArray(playerSize)) {
      return [];
    }

    if (Array.isArray(playerSize[0])) {
      playerSize = playerSize[0];
    }

    if (playerSize.length < 2) {
      return [];
    }

    return playerSize;
  }

  function getBidFloorData(bidRequest) {
    const { params } = bidRequest;
    const currency = params.currency || 'USD';

    let floorData;
    if (isFn(bidRequest.getFloor)) {
      const bidFloorRequest = {
        currency: currency,
        mediaType: VIDEO,
        size: '*'
      };
      floorData = bidRequest.getFloor(bidFloorRequest);
    } else if (params.bidFloor) {
      floorData = { floor: params.bidFloor, currency: currency };
    }

    return floorData;
  }

  function getRequestSite(bidRequest, bidderRequest) {
    const site = bidderRequest.ortb2.site || {};

    site.domain = site.domain || config.publisherDomain || window.location.hostname;
    site.page = site.page || config.pageUrl || window.location.href;

    const referer = bidderRequest.refererInfo && bidderRequest.refererInfo.referer;
    if (!site.ref && referer) {
      site.ref = referer;
    }

    const jwplayerPublisherExtChain = 'publisher.ext.jwplayer.';

    deepSetValue(site, jwplayerPublisherExtChain + 'publisherId', bidRequest.params.publisherId);
    deepSetValue(site, jwplayerPublisherExtChain + 'siteId', bidRequest.params.siteId);

    return site;
  }

  function getRequestDevice(ortb2) {
    const device = Object.assign({
      h: screen.height,
      w: screen.width,
      ua: navigator.userAgent,
      dnt: getDNT() ? 1 : 0,
      js: 1
    }, ortb2.device || {})

    const language = getLanguage();
    if (!device.language && language) {
      device.language = language;
    }

    return device;
  }

  function getLanguage() {
    const navigatorLanguage = navigator.language;
    if (!navigatorLanguage) {
      return;
    }

    const languageCodeSegments = navigatorLanguage.split('-');
    if (!languageCodeSegments.length) {
      return;
    }

    return languageCodeSegments[0];
  }

  function getRequestUser(ortb2) {
    const user = ortb2.user || {};
    if (config.getConfig('coppa') === true) {
      user.coppa = true;
    }

    return user;
  }

  function hasContentUrl(ortb2) {
    const site = ortb2.site;
    const content = site && site.content;
    return !!(content && content.url);
  }

  function getWarnings(bidderRequest) {
    const content = bidderRequest.ortb2.site.content;
    const contentChain = 'ortb2.site.content.';
    const warnings = [];
    if (!content.id) {
      warnings.push(getMissingFieldMessage(contentChain + 'id'));
    }

    if (!content.title) {
      warnings.push(getMissingFieldMessage(contentChain + 'title'));
    }

    if (!content.ext || !content.ext.description) {
      warnings.push(getMissingFieldMessage(contentChain + 'ext.description'));
    }

    return warnings;
  }

  function getMissingFieldMessage(fieldName) {
    return `Optional field ${fieldName} is not populated; we recommend populating for maximum performance.`
  }

  function logResponseWarnings(serverResponseBody) {
    const warningPayload = deepAccess(serverResponseBody, 'ext.warnings');
    if (!warningPayload) {
      return;
    }

    const warningCategories = Object.keys(warningPayload);
    warningCategories.forEach(category => {
      const warnings = warningPayload[category];
      if (!isArray(warnings)) {
        return;
      }

      warnings.forEach(warning => {
        logWarn(`${BIDDER_CODE}: [Bid Response][Warning Code: ${warning.code}] ${warning.message}`);
      });
    });
  }
}

export const spec = getBidAdapter();

registerBidder(spec);