prebid/Prebid.js

View on GitHub
modules/quantcastBidAdapter.js

Summary

Maintainability
C
1 day
Test Coverage
import {deepAccess, isArray, isEmpty, logError, logInfo} from '../src/utils.js';
import {ajax} from '../src/ajax.js';
import {config} from '../src/config.js';
import {getStorageManager} from '../src/storageManager.js';
import {registerBidder} from '../src/adapters/bidderFactory.js';
import {find} from '../src/polyfill.js';
import {parseDomain} from '../src/refererDetection.js';

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

const BIDDER_CODE = 'quantcast';
const DEFAULT_BID_FLOOR = 0.0000000001;

const QUANTCAST_VENDOR_ID = '11';
// Check other required purposes on server
const PURPOSE_DATA_COLLECT = '1';

export const QUANTCAST_DOMAIN = 'qcx.quantserve.com';
export const QUANTCAST_TEST_DOMAIN = 's2s-canary.quantserve.com';
export const QUANTCAST_NET_REVENUE = true;
export const QUANTCAST_TEST_PUBLISHER = 'test-publisher';
export const QUANTCAST_TTL = 4;
export const QUANTCAST_PROTOCOL = 'https';
export const QUANTCAST_PORT = '8443';
export const QUANTCAST_FPA = '__qca';

export const storage = getStorageManager({bidderCode: BIDDER_CODE});

function makeVideoImp(bid) {
  const videoInMediaType = deepAccess(bid, 'mediaTypes.video') || {};
  const videoInParams = deepAccess(bid, 'params.video') || {};
  const video = Object.assign({}, videoInParams, videoInMediaType);

  if (video.playerSize) {
    video.w = video.playerSize[0];
    video.h = video.playerSize[1];
  }
  const videoCopy = {
    mimes: video.mimes,
    minduration: video.minduration,
    maxduration: video.maxduration,
    protocols: video.protocols,
    startdelay: video.startdelay,
    linearity: video.linearity,
    battr: video.battr,
    maxbitrate: video.maxbitrate,
    playbackmethod: video.playbackmethod,
    delivery: video.delivery,
    api: video.api,
    w: video.w,
    h: video.h
  }

  return {
    video: videoCopy,
    placementCode: bid.placementCode,
    bidFloor: DEFAULT_BID_FLOOR
  };
}

function makeBannerImp(bid) {
  const sizes = bid.sizes || bid.mediaTypes.banner.sizes;

  return {
    banner: {
      battr: bid.params.battr,
      sizes: sizes.map(size => {
        return {
          width: size[0],
          height: size[1]
        };
      })
    },
    placementCode: bid.placementCode,
    bidFloor: DEFAULT_BID_FLOOR
  };
}

function checkTCF(tcData) {
  let restrictions = tcData.publisher ? tcData.publisher.restrictions : {};
  let qcRestriction = restrictions && restrictions[PURPOSE_DATA_COLLECT]
    ? restrictions[PURPOSE_DATA_COLLECT][QUANTCAST_VENDOR_ID]
    : null;

  if (qcRestriction === 0 || qcRestriction === 2) {
    // Not allowed by publisher, or requires legitimate interest
    return false;
  }

  let vendorConsent = tcData.vendor && tcData.vendor.consents && tcData.vendor.consents[QUANTCAST_VENDOR_ID];
  let purposeConsent = tcData.purpose && tcData.purpose.consents && tcData.purpose.consents[PURPOSE_DATA_COLLECT];

  return !!(vendorConsent && purposeConsent);
}

function getQuantcastFPA() {
  let fpa = storage.getCookie(QUANTCAST_FPA)
  return fpa || ''
}

let hasUserSynced = false;

/**
 * The documentation for Prebid.js Adapter 1.0 can be found at link below,
 * http://prebid.org/dev-docs/bidder-adapter-1.html
 */
export const spec = {
  code: BIDDER_CODE,
  GVLID: QUANTCAST_VENDOR_ID,
  supportedMediaTypes: ['banner', 'video'],

  /**
   * Verify the `AdUnits.bids` response with `true` for valid request and `false`
   * for invalid request.
   *
   * @param {object} bid
   * @return boolean `true` is this is a valid bid, and `false` otherwise
   */
  isBidRequestValid(bid) {
    return !!bid.params.publisherId;
  },

  /**
   * Make a server request when the page asks Prebid.js for bids from a list of
   * `BidRequests`.
   *
   * @param {BidRequest[]} bidRequests A non-empty list of bid requests which should be send to Quantcast server
   * @param bidderRequest
   * @return ServerRequest information describing the request to the server.
   */
  buildRequests(bidRequests, bidderRequest) {
    const bids = bidRequests || [];
    const gdprConsent = deepAccess(bidderRequest, 'gdprConsent') || {};
    const uspConsent = deepAccess(bidderRequest, 'uspConsent');
    const referrer = deepAccess(bidderRequest, 'refererInfo.ref');
    const page = deepAccess(bidderRequest, 'refererInfo.page') || deepAccess(window, 'location.href');
    const domain = parseDomain(page, {noLeadingWww: true});

    // Check for GDPR consent for purpose 1, and drop request if consent has not been given
    // Remaining consent checks are performed server-side.
    if (gdprConsent.gdprApplies) {
      if (gdprConsent.vendorData) {
        if (!checkTCF(gdprConsent.vendorData)) {
          logInfo(`${BIDDER_CODE}: No purpose 1 consent for TCF v2`);
          return;
        }
      }
    }

    let bidRequestsList = [];

    bids.forEach(bid => {
      let imp;
      if (bid.mediaTypes) {
        if (bid.mediaTypes.video && bid.mediaTypes.video.context === 'instream') {
          imp = makeVideoImp(bid);
        } else if (bid.mediaTypes.banner) {
          imp = makeBannerImp(bid);
        } else {
          // Unsupported mediaType
          logInfo(`${BIDDER_CODE}: No supported mediaTypes found in ${JSON.stringify(bid.mediaTypes)}`);
          return;
        }
      } else {
        // Parse as banner by default
        imp = makeBannerImp(bid);
      }

      // Request Data Format can be found at https://wiki.corp.qc/display/adinf/QCX
      const requestData = {
        publisherId: bid.params.publisherId,
        requestId: bid.bidId,
        imp: [imp],
        site: {
          page,
          referrer,
          domain
        },
        bidId: bid.bidId,
        gdprSignal: gdprConsent.gdprApplies ? 1 : 0,
        gdprConsent: gdprConsent.consentString,
        uspSignal: uspConsent ? 1 : 0,
        uspConsent,
        coppa: config.getConfig('coppa') === true ? 1 : 0,
        prebidJsVersion: '$prebid.version$',
        fpa: getQuantcastFPA()
      };

      const data = JSON.stringify(requestData);
      const qcDomain = bid.params.publisherId === QUANTCAST_TEST_PUBLISHER
        ? QUANTCAST_TEST_DOMAIN
        : QUANTCAST_DOMAIN;
      const url = `${QUANTCAST_PROTOCOL}://${qcDomain}:${QUANTCAST_PORT}/qchb`;

      bidRequestsList.push({
        data,
        method: 'POST',
        url
      });
    });

    return bidRequestsList;
  },

  /**
   * Function get called when the browser has received the response from Quantcast server.
   * The function parse the response and create a `bidResponse` object containing one/more bids.
   * Returns an empty array if no valid bids
   *
   * Response Data Format can be found at https://wiki.corp.qc/display/adinf/QCX
   *
   * @param {*} serverResponse A successful response from Quantcast server.
   * @return {Bid[]} An array of bids which were nested inside the server.
   *
   */
  interpretResponse(serverResponse) {
    if (serverResponse === undefined) {
      logError('Server Response is undefined');
      return [];
    }

    const response = serverResponse['body'];

    if (response === undefined || !response.hasOwnProperty('bids')) {
      logError('Sub-optimal JSON received from Quantcast server');
      return [];
    }

    if (isEmpty(response.bids)) {
      // Shortcut response handling if no bids are present
      return [];
    }

    const bidResponsesList = response.bids.map(bid => {
      const { ad, cpm, width, height, creativeId, currency, videoUrl, dealId, meta } = bid;

      const result = {
        requestId: response.requestId,
        cpm,
        width,
        height,
        ad,
        ttl: QUANTCAST_TTL,
        creativeId,
        netRevenue: QUANTCAST_NET_REVENUE,
        currency
      };

      if (videoUrl !== undefined && videoUrl) {
        result['vastUrl'] = videoUrl;
        result['mediaType'] = 'video';
      }

      if (dealId !== undefined && dealId) {
        result['dealId'] = dealId;
      }

      if (meta !== undefined && meta.advertiserDomains && isArray(meta.advertiserDomains)) {
        result.meta = {};
        result.meta.advertiserDomains = meta.advertiserDomains;
      }

      return result;
    });

    return bidResponsesList;
  },
  onTimeout(timeoutData) {
    const url = `${QUANTCAST_PROTOCOL}://${QUANTCAST_DOMAIN}:${QUANTCAST_PORT}/qchb_notify?type=timeout`;
    ajax(url, null, null);
  },
  getUserSyncs(syncOptions, serverResponses) {
    const syncs = []
    if (!hasUserSynced && syncOptions.pixelEnabled) {
      const responseWithUrl = find(serverResponses, serverResponse =>
        deepAccess(serverResponse.body, 'userSync.url')
      );

      if (responseWithUrl) {
        const url = deepAccess(responseWithUrl.body, 'userSync.url')
        syncs.push({
          type: 'image',
          url: url
        });
      }
      hasUserSynced = true;
    }
    return syncs;
  },
  resetUserSync() {
    hasUserSynced = false;
  }
};

registerBidder(spec);