prebid/Prebid.js

View on GitHub
modules/cwireBidAdapter.js

Summary

Maintainability
A
3 hrs
Test Coverage
import {registerBidder} from '../src/adapters/bidderFactory.js';
import {getStorageManager} from '../src/storageManager.js';
import {BANNER} from '../src/mediaTypes.js';
import {generateUUID, getParameterByName, isNumber, logError, logInfo} from '../src/utils.js';
import {hasPurpose1Consent} from '../src/utils/gpdr.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
 */

// ------------------------------------
const BIDDER_CODE = 'cwire';
const CWID_KEY = 'cw_cwid';

export const BID_ENDPOINT = 'https://prebid.cwi.re/v1/bid';
export const EVENT_ENDPOINT = 'https://prebid.cwi.re/v1/event';
export const GVL_ID = 1081;

/**
 * Allows limiting ad impressions per site render. Unique per prebid instance ID.
 */
export const pageViewId = generateUUID();

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

/**
 * Retrieve dimensions and CSS max height/width from a given slot and attach the properties to the bidRequest.
 * @param bid
 * @returns {*&{cwExt: {dimensions: {width: number, height: number}, style: {maxWidth: number, maxHeight: number}}}}
 */
function slotDimensions(bid) {
  let adUnitCode = bid.adUnitCode;
  let slotEl = document.getElementById(adUnitCode);

  if (slotEl) {
    logInfo(`Slot element found: ${adUnitCode}`)
    const slotW = slotEl.offsetWidth
    const slotH = slotEl.offsetHeight
    const cssMaxW = slotEl.style?.maxWidth;
    const cssMaxH = slotEl.style?.maxHeight;
    logInfo(`Slot dimensions (w/h): ${slotW} / ${slotH}`)
    logInfo(`Slot Styles (maxW/maxH): ${cssMaxW} / ${cssMaxH}`)

    bid = {
      ...bid,
      cwExt: {
        dimensions: {
          width: slotW,
          height: slotH,
        },
        style: {
          ...(cssMaxW) && {
            maxWidth: cssMaxW
          },
          ...(cssMaxH) && {
            maxHeight: cssMaxH
          }
        }
      }
    }
  }
  return bid
}

/**
 * Extracts feature flags from a comma-separated url parameter `cwfeatures`.
 *
 * @returns *[]
 */
function getFeatureFlags() {
  let ffParam = getParameterByName('cwfeatures')
  if (ffParam) {
    return ffParam.split(',')
  }
  return []
}

function getRefGroups() {
  const groups = getParameterByName('cwgroups')
  if (groups) {
    return groups.split(',')
  }
  return []
}

/**
 * Reads the CWID from local storage.
 */
function getCwid() {
  return storage.localStorageIsEnabled() ? storage.getDataFromLocalStorage(CWID_KEY) : null;
}

function hasCwid() {
  return storage.localStorageIsEnabled() && storage.getDataFromLocalStorage(CWID_KEY);
}

/**
 * Store the CWID to local storage.
 */
function updateCwid(cwid) {
  if (storage.localStorageIsEnabled()) {
    storage.setDataInLocalStorage(CWID_KEY, cwid)
  } else {
    logInfo(`Could not set CWID ${cwid} in localstorage`);
  }
}

/**
 * Extract and collect cwire specific extensions.
 */
function getCwExtension() {
  const cwId = getCwid();
  const cwCreative = getParameterByName('cwcreative')
  const cwGroups = getRefGroups()
  const cwFeatures = getFeatureFlags();
  // Enable debug flag by passing ?cwdebug=true as url parameter.
  // Note: pbjs_debug=true enables it on prebid level
  // More info: https://docs.prebid.org/troubleshooting/troubleshooting-guide.html#turn-on-prebidjs-debug-messages
  const debug = getParameterByName('cwdebug');

  return {
    ...(cwId) && {
      cwid: cwId
    },
    ...(cwGroups.length > 0) && {
      refgroups: cwGroups
    },
    ...(cwFeatures.length > 0) && {
      featureFlags: cwFeatures
    },
    ...(cwCreative) && {
      cwcreative: cwCreative
    },
    ...(debug) && {
      debug: true
    }
  };
}

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

  /**
   * 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) {
    if (!bid.params?.placementId || !isNumber(bid.params.placementId)) {
      logError('placementId not provided or not a number');
      return false;
    }

    if (!bid.params?.pageId || !isNumber(bid.params.pageId)) {
      logError('pageId not provided or not a number');
      return false;
    }
    return true;
  },

  /**
   * Make a server request from the list of BidRequests.
   *
   * @param {validBidRequests[]} validBidRequests An array of bids.
   * @return ServerRequest Info describing the request to the server.
   */
  buildRequests: function (validBidRequests, bidderRequest) {
    // There are more fields on the refererInfo object
    let referrer = bidderRequest?.refererInfo?.page

    // process bid requests
    let processed = validBidRequests
      .map(bid => slotDimensions(bid))
      // Flattens the pageId and placement Id for backwards compatibility.
      .map((bid) => ({...bid, pageId: bid.params?.pageId, placementId: bid.params?.placementId}));

    const extensions = getCwExtension();
    const payload = {
      slots: processed,
      httpRef: referrer,
      // TODO: Verify whether the auctionId and the usage of pageViewId make sense.
      pageViewId: pageViewId,
      sdk: {
        version: '$prebid.version$'
      },
      ...extensions
    };
    const payloadString = JSON.stringify(payload);
    return {
      method: 'POST',
      url: BID_ENDPOINT,
      data: payloadString,
    };
  },
  /**
   * Unpack the response from the server into a list of bids.
   *
   * @param {ServerResponse} serverResponse A successful response from the server.
   * @return {Bid[]} An array of bids which were nested inside the server.
   */
  interpretResponse: function (serverResponse, bidRequest) {
    if (!hasCwid()) {
      const cwid = serverResponse.body?.cwid
      if (cwid) {
        updateCwid(cwid);
      }
    }

    // Rename `html` response property to `ad` as used by prebid.
    const bids = serverResponse.body?.bids.map(({html, ...rest}) => ({...rest, ad: html}));
    return bids || [];
  },

  onBidWon: function (bid) {
    logInfo(`Bid won.`)
    const event = {
      type: 'BID_WON',
      payload: {
        bid: bid
      }
    }
    navigator.sendBeacon(EVENT_ENDPOINT, JSON.stringify(event))
  },

  onBidderError: function (error, bidderRequest) {
    logInfo(`Bidder error: ${error}`)
    const event = {
      type: 'BID_ERROR',
      payload: {
        error: error,
        bidderRequest: bidderRequest
      }
    }
    navigator.sendBeacon(EVENT_ENDPOINT, JSON.stringify(event))
  },

  getUserSyncs: function(syncOptions, serverResponses, gdprConsent, uspConsent) {
    logInfo('Collecting user-syncs: ', JSON.stringify({syncOptions, gdprConsent, uspConsent, serverResponses}));

    const syncs = []
    if (hasPurpose1Consent(gdprConsent)) {
      logInfo('GDPR purpose 1 consent was given, adding user-syncs')
      let type = (syncOptions.pixelEnabled) ? 'image' : null ?? (syncOptions.iframeEnabled) ? 'iframe' : null
      if (type) {
        syncs.push({
          type: type,
          url: 'https://ib.adnxs.com/getuid?https://prebid.cwi.re/v1/cookiesync?xandrId=$UID'
        })
      }
    }
    logInfo('Collected user-syncs: ', JSON.stringify({syncs}))
    return syncs
  }

};
registerBidder(spec);