prebid/Prebid.js

View on GitHub
modules/zeta_global_sspBidAdapter.js

Summary

Maintainability
F
1 wk
Test Coverage
import {deepAccess, deepSetValue, isArray, isBoolean, isNumber, isStr, logWarn} from '../src/utils.js';
import {registerBidder} from '../src/adapters/bidderFactory.js';
import {BANNER, VIDEO} from '../src/mediaTypes.js';
import {config} from '../src/config.js';
import {parseDomain} from '../src/refererDetection.js';

/**
 * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest
 * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid
 * @typedef {import('../src/adapters/bidderFactory.js').ServerResponse} ServerResponse
 * @typedef {import('../src/adapters/bidderFactory.js').Bids} Bids
 * @typedef {import('../src/adapters/bidderFactory.js').BidderRequest} BidderRequest
 */

const BIDDER_CODE = 'zeta_global_ssp';
const ENDPOINT_URL = 'https://ssp.disqus.com/bid/prebid';
const USER_SYNC_URL_IFRAME = 'https://ssp.disqus.com/sync?type=iframe';
const USER_SYNC_URL_IMAGE = 'https://ssp.disqus.com/sync?type=image';
const DEFAULT_CUR = 'USD';
const TTL = 300;
const NET_REV = true;

const DATA_TYPES = {
  'NUMBER': 'number',
  'STRING': 'string',
  'BOOLEAN': 'boolean',
  'ARRAY': 'array',
  'OBJECT': 'object'
};
const VIDEO_CUSTOM_PARAMS = {
  'mimes': DATA_TYPES.ARRAY,
  'minduration': DATA_TYPES.NUMBER,
  'maxduration': DATA_TYPES.NUMBER,
  'startdelay': DATA_TYPES.NUMBER,
  'playbackmethod': DATA_TYPES.ARRAY,
  'api': DATA_TYPES.ARRAY,
  'protocols': DATA_TYPES.ARRAY,
  'w': DATA_TYPES.NUMBER,
  'h': DATA_TYPES.NUMBER,
  'battr': DATA_TYPES.ARRAY,
  'linearity': DATA_TYPES.NUMBER,
  'placement': DATA_TYPES.NUMBER,
  'plcmt': DATA_TYPES.NUMBER,
  'minbitrate': DATA_TYPES.NUMBER,
  'maxbitrate': DATA_TYPES.NUMBER,
  'skip': DATA_TYPES.NUMBER
}

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

  /**
   * Determines whether the given bid request is valid.
   *
   * @param {BidRequest} bid The bid params to validate.
   * @return boolean True if this is a valid bid, and false otherwise.
   */
  isBidRequestValid: function (bid) {
    // check for all required bid fields
    if (!(bid &&
      bid.bidId &&
      bid.params &&
      bid.params.sid)) {
      logWarn('Invalid bid request - missing required bid data');
      return false;
    }
    return true;
  },

  /**
   * Make a server request from the list of BidRequests.
   *
   * @param {Bids[]} validBidRequests - an array of bidRequest objects
   * @param {BidderRequest} bidderRequest - master bidRequest object
   * @return ServerRequest Info describing the request to the server.
   */
  buildRequests: function (validBidRequests, bidderRequest) {
    const secure = 1; // treat all requests as secure
    const params = validBidRequests[0].params;
    const imps = validBidRequests.map(request => {
      const impData = {
        id: request.bidId,
        secure: secure
      };
      const tagid = request.params?.tagid;
      if (tagid) {
        impData.tagid = tagid;
      }
      if (request.mediaTypes) {
        for (const mediaType in request.mediaTypes) {
          switch (mediaType) {
            case BANNER:
              impData.banner = buildBanner(request);
              break;
            case VIDEO:
              impData.video = buildVideo(request);
              break;
          }
        }
      }
      if (!impData.banner && !impData.video) {
        impData.banner = buildBanner(request);
      }

      if (typeof request.getFloor === 'function') {
        const floorInfo = request.getFloor({
          currency: 'USD',
          mediaType: impData.video ? 'video' : 'banner',
          size: [ impData.video ? impData.video.w : impData.banner.w, impData.video ? impData.video.h : impData.banner.h ]
        });
        if (floorInfo && floorInfo.floor) {
          impData.bidfloor = floorInfo.floor;
        }
      }
      if (!impData.bidfloor) {
        const bidfloor = request.params?.bidfloor;
        if (bidfloor) {
          impData.bidfloor = bidfloor;
        }
      }

      return impData;
    });

    let payload = {
      id: bidderRequest.bidderRequestId,
      cur: [DEFAULT_CUR],
      imp: imps,
      site: params.site ? params.site : {},
      device: {...(bidderRequest.ortb2?.device || {}), ...params.device},
      user: params.user ? params.user : {},
      app: params.app ? params.app : {},
      ext: {
        tags: params.tags ? params.tags : {},
        sid: params.sid ? params.sid : undefined
      }
    };
    const rInfo = bidderRequest.refererInfo;
    // TODO: do the fallbacks make sense here?
    payload.site.page = cropPage(rInfo.page || rInfo.topmostLocation);
    payload.site.domain = parseDomain(payload.site.page, {noLeadingWww: true});

    payload.device.ua = navigator.userAgent;
    payload.device.language = navigator.language;
    payload.device.w = screen.width;
    payload.device.h = screen.height;

    if (bidderRequest.ortb2?.user?.geo && bidderRequest.ortb2?.device?.geo) {
      payload.device.geo = { ...payload.device.geo, ...bidderRequest.ortb2?.device.geo };
      payload.user.geo = { ...payload.user.geo, ...bidderRequest.ortb2?.user.geo };
    } else {
      if (bidderRequest.ortb2?.user?.geo) {
        payload.user.geo = payload.device.geo = { ...payload.user.geo, ...bidderRequest.ortb2?.user.geo };
      }
      if (bidderRequest.ortb2?.device?.geo) {
        payload.user.geo = payload.device.geo = { ...payload.user.geo, ...bidderRequest.ortb2?.device.geo };
      }
    }

    if (bidderRequest?.ortb2?.device?.sua) {
      payload.device.sua = bidderRequest.ortb2.device.sua;
    }

    if (params.test) {
      payload.test = params.test;
    }

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

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

    // schain
    if (validBidRequests[0].schain) {
      payload.source = {
        ext: {
          schain: validBidRequests[0].schain
        }
      }
    }

    if (bidderRequest?.timeout) {
      payload.tmax = bidderRequest.timeout;
    }

    provideEids(validBidRequests[0], payload);
    provideSegments(bidderRequest, payload);
    const url = params.sid ? ENDPOINT_URL.concat('?sid=', params.sid) : ENDPOINT_URL;
    return {
      method: 'POST',
      url: url,
      data: JSON.stringify(clearEmpties(payload)),
    };
  },

  /**
   * Unpack the response from the server into a list of bids.
   *
   * @param {ServerResponse} serverResponse A successful response from the server.
   * @param bidRequest The payload from the server's response.
   * @return {Bid[]} An array of bids which were nested inside the server.
   */
  interpretResponse: function (serverResponse, bidRequest) {
    let bidResponses = [];
    const response = (serverResponse || {}).body;
    if (response && response.seatbid && response.seatbid[0].bid && response.seatbid[0].bid.length) {
      response.seatbid.forEach(zetaSeatbid => {
        const seat = zetaSeatbid.seat;
        zetaSeatbid.bid.forEach(zetaBid => {
          let bid = {
            requestId: zetaBid.impid,
            cpm: zetaBid.price,
            currency: response.cur,
            width: zetaBid.w,
            height: zetaBid.h,
            ad: zetaBid.adm,
            ttl: TTL,
            creativeId: zetaBid.crid,
            netRevenue: NET_REV,
          };
          if (zetaBid.adomain && zetaBid.adomain.length) {
            bid.meta = {
              advertiserDomains: zetaBid.adomain
            };
          }
          provideMediaType(zetaBid, bid, bidRequest.data);
          if (bid.mediaType === VIDEO) {
            bid.vastXml = bid.ad;
          }
          if (seat) {
            bid.dspId = seat;
          }
          bidResponses.push(bid);
        })
      })
    }
    return bidResponses;
  },

  /**
   * Register User Sync.
   */
  getUserSyncs: (syncOptions, responses, gdprConsent, uspConsent) => {
    let syncurl = '';

    // Attaching GDPR Consent Params in UserSync url
    if (gdprConsent) {
      syncurl += '&gdpr=' + (gdprConsent.gdprApplies ? 1 : 0);
      syncurl += '&gdpr_consent=' + encodeURIComponent(gdprConsent.consentString || '');
    }

    // CCPA
    if (uspConsent) {
      syncurl += '&us_privacy=' + encodeURIComponent(uspConsent);
    }

    // coppa compliance
    if (config.getConfig('coppa') === true) {
      syncurl += '&coppa=1';
    }

    if (syncOptions.iframeEnabled) {
      return [{
        type: 'iframe',
        url: USER_SYNC_URL_IFRAME + syncurl
      }];
    } else {
      return [{
        type: 'image',
        url: USER_SYNC_URL_IMAGE + syncurl
      }];
    }
  }
}

function buildBanner(request) {
  let sizes = request.sizes;
  if (request.mediaTypes &&
    request.mediaTypes.banner &&
    request.mediaTypes.banner.sizes) {
    sizes = request.mediaTypes.banner.sizes;
  }
  if (sizes.length > 1) {
    const format = sizes.map(s => {
      return {
        w: s[0],
        h: s[1]
      }
    });
    return {
      w: sizes[0][0],
      h: sizes[0][1],
      format: format
    }
  } else {
    return {
      w: sizes[0][0],
      h: sizes[0][1]
    }
  }
}

function buildVideo(request) {
  let video = {};
  const videoParams = deepAccess(request, 'mediaTypes.video', {});
  for (const key in VIDEO_CUSTOM_PARAMS) {
    if (videoParams.hasOwnProperty(key)) {
      video[key] = checkParamDataType(key, videoParams[key], VIDEO_CUSTOM_PARAMS[key]);
    }
  }
  if (videoParams.playerSize) {
    if (isArray(videoParams.playerSize[0])) {
      video.w = parseInt(videoParams.playerSize[0][0], 10);
      video.h = parseInt(videoParams.playerSize[0][1], 10);
    } else if (isNumber(videoParams.playerSize[0])) {
      video.w = parseInt(videoParams.playerSize[0], 10);
      video.h = parseInt(videoParams.playerSize[1], 10);
    }
  }
  return video;
}

function checkParamDataType(key, value, datatype) {
  let functionToExecute;
  switch (datatype) {
    case DATA_TYPES.BOOLEAN:
      functionToExecute = isBoolean;
      break;
    case DATA_TYPES.NUMBER:
      functionToExecute = isNumber;
      break;
    case DATA_TYPES.STRING:
      functionToExecute = isStr;
      break;
    case DATA_TYPES.ARRAY:
      functionToExecute = isArray;
      break;
  }
  if (functionToExecute(value)) {
    return value;
  }
  logWarn('Ignoring param key: ' + key + ', expects ' + datatype + ', found ' + typeof value);
  return undefined;
}

function provideEids(request, payload) {
  if (Array.isArray(request.userIdAsEids) && request.userIdAsEids.length > 0) {
    deepSetValue(payload, 'user.ext.eids', request.userIdAsEids);
  }
}

function provideSegments(bidderRequest, payload) {
  const data = bidderRequest.ortb2?.user?.data;
  if (isArray(data)) {
    const segments = data.filter(d => d?.segment).map(d => d.segment).filter(s => isArray(s)).flatMap(s => s).filter(s => s?.id);
    if (segments.length > 0) {
      if (!payload.user) {
        payload.user = {};
      }
      if (!isArray(payload.user.data)) {
        payload.user.data = [];
      }
      const payloadData = {
        segment: segments
      };
      payload.user.data.push(payloadData);
    }
  }
}

function provideMediaType(zetaBid, bid, bidRequest) {
  if (zetaBid.ext && zetaBid.ext.prebid && zetaBid.ext.prebid.type) {
    bid.mediaType = zetaBid.ext.prebid.type === VIDEO ? VIDEO : BANNER;
  } else {
    bid.mediaType = bidRequest.imp[0].video ? VIDEO : BANNER;
  }
}

function cropPage(page) {
  if (page) {
    if (page.length > 100) {
      page = page.substring(0, 100);
    }
    if (page.startsWith('https://')) {
      page = page.substring(8);
    } else if (page.startsWith('http://')) {
      page = page.substring(7);
    }
    if (page.startsWith('www.')) {
      page = page.substring(4);
    }
    for (let i = 3; i < page.length; i++) {
      const c = page[i];
      if (c === '#' || c === '?') {
        return page.substring(0, i);
      }
    }
    return page;
  }
  return '';
}

function clearEmpties(o) {
  for (let k in o) {
    if (o[k] === null) {
      delete o[k];
      continue;
    }
    if (!o[k] || typeof o[k] !== 'object') {
      continue;
    }
    clearEmpties(o[k]);
    if (Object.keys(o[k]).length === 0) {
      delete o[k];
    }
  }
  return o;
}

registerBidder(spec);