prebid/Prebid.js

View on GitHub
modules/concertBidAdapter.js

Summary

Maintainability
A
3 hrs
Test Coverage
import { logWarn, logMessage, debugTurnedOn, generateUUID, deepAccess } from '../src/utils.js';
import { registerBidder } from '../src/adapters/bidderFactory.js';
import { getStorageManager } from '../src/storageManager.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
 * @typedef {import('../src/adapters/bidderFactory.js').validBidRequests} validBidRequests
 */

const BIDDER_CODE = 'concert';
const CONCERT_ENDPOINT = 'https://bids.concert.io';

export const spec = {
  code: BIDDER_CODE,
  /**
   * Determines whether or not 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.partnerId) {
      logWarn('Missing partnerId bid parameter');
      return false;
    }

    return true;
  },

  /**
   * Make a server request from the list of BidRequests.
   *
   * @param {validBidRequests[]} - an array of bids
   * @param {bidderRequest} -
   * @return ServerRequest Info describing the request to the server.
   */
  buildRequests: function(validBidRequests, bidderRequest) {
    logMessage(validBidRequests);
    logMessage(bidderRequest);

    const eids = [];

    let payload = {
      meta: {
        prebidVersion: '$prebid.version$',
        pageUrl: bidderRequest.refererInfo.page,
        screen: [window.screen.width, window.screen.height].join('x'),
        browserLanguage: window.navigator.language,
        debug: debugTurnedOn(),
        uid: getUid(bidderRequest, validBidRequests),
        optedOut: hasOptedOutOfPersonalization(),
        adapterVersion: '1.2.0',
        uspConsent: bidderRequest.uspConsent,
        gdprConsent: bidderRequest.gdprConsent,
        gppConsent: bidderRequest.gppConsent,
        tdid: getTdid(bidderRequest, validBidRequests),
      }
    };

    if (!payload.meta.gppConsent && bidderRequest.ortb2?.regs?.gpp) {
      payload.meta.gppConsent = {
        gppString: bidderRequest.ortb2.regs.gpp,
        applicableSections: bidderRequest.ortb2.regs.gpp_sid
      }
    }

    payload.slots = validBidRequests.map(bidRequest => {
      collectEid(eids, bidRequest);
      const adUnitElement = document.getElementById(bidRequest.adUnitCode)
      const coordinates = getOffset(adUnitElement)

      let slot = {
        name: bidRequest.adUnitCode,
        bidId: bidRequest.bidId,
        transactionId: bidRequest.ortb2Imp?.ext?.tid,
        sizes: bidRequest.params.sizes || bidRequest.sizes,
        partnerId: bidRequest.params.partnerId,
        slotType: bidRequest.params.slotType,
        adSlot: bidRequest.params.slot || bidRequest.adUnitCode,
        placementId: bidRequest.params.placementId || '',
        site: bidRequest.params.site || bidderRequest.refererInfo.page,
        ref: bidderRequest.refererInfo.ref,
        offsetCoordinates: { x: coordinates?.left, y: coordinates?.top }
      }

      return slot;
    });

    payload.meta.eids = eids.filter(Boolean);

    logMessage(payload);

    return {
      method: 'POST',
      url: `${CONCERT_ENDPOINT}/bids/prebid`,
      data: JSON.stringify(payload)
    };
  },
  /**
   * 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) {
    logMessage(serverResponse);
    logMessage(bidRequest);

    const serverBody = serverResponse.body;

    if (!serverBody || typeof serverBody !== 'object') {
      return [];
    }

    let bidResponses = [];

    bidResponses = serverBody.bids.map(bid => {
      return {
        requestId: bid.bidId,
        cpm: bid.cpm,
        width: bid.width,
        height: bid.height,
        ad: bid.ad,
        ttl: bid.ttl,
        meta: { advertiserDomains: bid && bid.adomain ? bid.adomain : [] },
        creativeId: bid.creativeId,
        netRevenue: bid.netRevenue,
        currency: bid.currency,
        ...(bid.dealid && { dealId: bid.dealid }),
      };
    });

    if (debugTurnedOn() && serverBody.debug) {
      logMessage(`CONCERT`, serverBody.debug);
    }

    logMessage(bidResponses);
    return bidResponses;
  },

  /**
   * Register bidder specific code, which will execute if bidder timed out after an auction
   * @param {data} Containing timeout specific data
   */
  onTimeout: function(data) {
    logMessage('concert bidder timed out');
    logMessage(data);
  },

  /**
   * Register bidder specific code, which will execute if a bid from this bidder won the auction
   * @param {Bid} The bid that won the auction
   */
  onBidWon: function(bid) {
    logMessage('concert bidder won bid');
    logMessage(bid);
  }

}

registerBidder(spec);

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

/**
 * Check or generate a UID for the current user.
 */
function getUid(bidderRequest, validBidRequests) {
  if (hasOptedOutOfPersonalization() || !consentAllowsPpid(bidderRequest)) {
    return false;
  }

  /**
   * check for shareId or pubCommonId before generating a new one
   * sharedId: @see https://docs.prebid.org/dev-docs/modules/userId.html
   * pubCid (no longer supported): @see https://docs.prebid.org/dev-docs/modules/pubCommonId.html#adapter-integration
   */
  const sharedId =
    deepAccess(validBidRequests[0], 'userId.sharedid.id') ||
    deepAccess(validBidRequests[0], 'userId.pubcid')
  const pubCid = deepAccess(validBidRequests[0], 'crumbs.pubcid');

  if (sharedId) return sharedId;
  if (pubCid) return pubCid;

  const LEGACY_CONCERT_UID_KEY = 'c_uid';
  const CONCERT_UID_KEY = 'vmconcert_uid';

  const legacyUid = storage.getDataFromLocalStorage(LEGACY_CONCERT_UID_KEY);
  let uid = storage.getDataFromLocalStorage(CONCERT_UID_KEY);

  if (legacyUid) {
    uid = legacyUid;
    storage.setDataInLocalStorage(CONCERT_UID_KEY, uid);
    storage.removeDataFromLocalStorage(LEGACY_CONCERT_UID_KEY);
  }

  if (!uid) {
    uid = generateUUID();
    storage.setDataInLocalStorage(CONCERT_UID_KEY, uid);
  }

  return uid;
}

/**
 * Whether the user has opted out of personalization.
 */
function hasOptedOutOfPersonalization() {
  const CONCERT_NO_PERSONALIZATION_KEY = 'c_nap';

  return storage.getDataFromLocalStorage(CONCERT_NO_PERSONALIZATION_KEY) === 'true';
}

/**
 * Whether the privacy consent strings allow personalization.
 *
 * @param {BidderRequest} bidderRequest Object which contains any data consent signals
 */
function consentAllowsPpid(bidderRequest) {
  let uspConsentAllows = true;

  // if a us privacy string was provided, but they explicitly opted out
  if (
    typeof bidderRequest?.uspConsent === 'string' &&
    bidderRequest?.uspConsent[0] === '1' &&
    bidderRequest?.uspConsent[2].toUpperCase() === 'Y' // user has opted-out
  ) {
    uspConsentAllows = false;
  }

  /*
   * True if the gdprConsent is null-y; or GDPR does not apply; or if purpose 1 consent was given.
   * Much more nuanced GDPR requirements are tested on the bid server using the @iabtcf/core npm module;
   */
  const gdprConsentAllows = hasPurpose1Consent(bidderRequest?.gdprConsent);

  return (uspConsentAllows && gdprConsentAllows);
}

function collectEid(eids, bid) {
  if (bid.userId) {
    const eid = getUserId(bid.userId.uid2 && bid.userId.uid2.id, 'uidapi.com', undefined, 3)
    eids.push(eid)
  }
}

function getUserId(id, source, uidExt, atype) {
  if (id) {
    const uid = { id, atype };

    if (uidExt) {
      uid.ext = uidExt;
    }

    return {
      source,
      uids: [ uid ]
    };
  }
}

function getOffset(el) {
  if (el) {
    const rect = el.getBoundingClientRect();
    return {
      left: rect.left + window.scrollX,
      top: rect.top + window.scrollY
    };
  }
}

function getTdid(bidderRequest, validBidRequests) {
  if (hasOptedOutOfPersonalization() || !consentAllowsPpid(bidderRequest)) {
    return null;
  }

  return deepAccess(validBidRequests[0], 'userId.tdid') || null;
}