prebid/Prebid.js

View on GitHub
modules/ooloAnalyticsAdapter.js

Summary

Maintainability
C
1 day
Test Coverage
import { _each, deepClone, pick, deepSetValue, logError, logInfo } from '../src/utils.js';
import { getOrigin } from '../libraries/getOrigin/index.js';
import adapter from '../libraries/analyticsAdapter/AnalyticsAdapter.js'
import adapterManager from '../src/adapterManager.js'
import { EVENTS } from '../src/constants.js'
import { ajax } from '../src/ajax.js'
import { config } from '../src/config.js'

const baseUrl = 'https://pbdata.oolo.io/'
const ENDPOINTS = {
  CONFIG: 'https://config-pbdata.oolo.io',
  PAGE_DATA: baseUrl + 'page',
  AUCTION: baseUrl + 'auctionData',
  BID_WON: baseUrl + 'bidWonData',
  AD_RENDER_FAILED: baseUrl + 'adRenderFailedData',
  RAW: baseUrl + 'raw',
  HBCONFIG: baseUrl + 'hbconfig',
}

const pbModuleVersion = '1.0.0'
const prebidVersion = '$prebid.version$'
const analyticsType = 'endpoint'
const ADAPTER_CODE = 'oolo'
const AUCTION_END_SEND_TIMEOUT = 1500
export const PAGEVIEW_ID = +generatePageViewId()

const {
  AUCTION_INIT,
  AUCTION_END,
  BID_REQUESTED,
  BID_RESPONSE,
  NO_BID,
  BID_WON,
  BID_TIMEOUT,
  AD_RENDER_FAILED
} = EVENTS

const SERVER_EVENTS = {
  AUCTION: 'auction',
  WON: 'bidWon',
  AD_RENDER_FAILED: 'adRenderFailed'
}

const SERVER_BID_STATUS = {
  NO_BID: 'noBid',
  BID_REQUESTED: 'bidRequested',
  BID_RECEIVED: 'bidReceived',
  BID_TIMEDOUT: 'bidTimedOut',
  BID_WON: 'bidWon'
}

let auctions = {}
let initOptions = {}
let eventsQueue = []

const onAuctionInit = (args) => {
  const { auctionId, adUnits, timestamp } = args

  let auction = auctions[auctionId] = {
    ...args,
    adUnits: {},
    auctionStart: timestamp,
    _sentToServer: false
  }

  handleCustomFields(auction, AUCTION_INIT, args)

  _each(adUnits, adUnit => {
    auction.adUnits[adUnit.code] = {
      ...adUnit,
      auctionId,
      adunid: adUnit.code,
      bids: {},
    }
  })
}

const onBidRequested = (args) => {
  const { auctionId, bids, start, timeout } = args
  const _start = start || Date.now()
  const auction = auctions[auctionId]
  const auctionAdUnits = auction.adUnits

  bids.forEach(bid => {
    const { adUnitCode } = bid
    const bidId = parseBidId(bid)

    auctionAdUnits[adUnitCode].bids[bidId] = {
      ...bid,
      timeout,
      start: _start,
      rs: _start - auction.auctionStart,
      bidStatus: SERVER_BID_STATUS.BID_REQUESTED,
    }
  })
}

const onBidResponse = (args) => {
  const { auctionId, adUnitCode } = args
  const auction = auctions[auctionId]
  const bidId = parseBidId(args)
  let bid = auction.adUnits[adUnitCode].bids[bidId]

  Object.assign(bid, args, {
    bidStatus: SERVER_BID_STATUS.BID_RECEIVED,
    end: args.responseTimestamp,
    re: args.responseTimestamp - auction.auctionStart
  })
}

const onNoBid = (args) => {
  const { auctionId, adUnitCode } = args
  const bidId = parseBidId(args)
  const end = Date.now()
  const auction = auctions[auctionId]
  let bid = auction.adUnits[adUnitCode].bids[bidId]

  Object.assign(bid, args, {
    bidStatus: SERVER_BID_STATUS.NO_BID,
    end,
    re: end - auction.auctionStart
  })
}

const onBidWon = (args) => {
  const { auctionId, adUnitCode } = args
  const bidId = parseBidId(args)
  const bid = auctions[auctionId].adUnits[adUnitCode].bids[bidId]

  Object.assign(bid, args, {
    bidStatus: SERVER_BID_STATUS.BID_WON,
    isW: true,
    isH: true
  })

  if (auctions[auctionId]._sentToServer) {
    const payload = {
      auctionId,
      adunid: adUnitCode,
      bid: mapBid(bid, BID_WON)
    }

    sendEvent(SERVER_EVENTS.WON, payload)
  }
}

const onBidTimeout = (args) => {
  _each(args, bid => {
    const { auctionId, adUnitCode } = bid
    const bidId = parseBidId(bid)
    let bidCache = auctions[auctionId].adUnits[adUnitCode].bids[bidId]

    Object.assign(bidCache, bid, {
      bidStatus: SERVER_BID_STATUS.BID_TIMEDOUT,
    })
  })
}

const onAuctionEnd = (args) => {
  const { auctionId, adUnits, ...restAuctionEnd } = args

  Object.assign(auctions[auctionId], restAuctionEnd, {
    auctionEnd: args.auctionEnd || Date.now()
  })

  // wait for bidWon before sending to server
  setTimeout(() => {
    auctions[auctionId]._sentToServer = true
    const finalAuctionData = buildAuctionData(auctions[auctionId])

    sendEvent(SERVER_EVENTS.AUCTION, finalAuctionData)
  }, initOptions.serverConfig.BID_WON_TIMEOUT || AUCTION_END_SEND_TIMEOUT)
}

const onAdRenderFailed = (args) => {
  const data = deepClone(args)
  data.timestamp = Date.now()

  if (data.bid) {
    data.bid = mapBid(data.bid, AD_RENDER_FAILED)
  }

  sendEvent(SERVER_EVENTS.AD_RENDER_FAILED, data)
}

var ooloAdapter = Object.assign(
  adapter({ analyticsType }), {
    track({ eventType, args }) {
      // wait for server configuration before processing the events
      if (typeof initOptions.serverConfig !== 'undefined' && eventsQueue.length === 0) {
        handleEvent(eventType, args)
      } else {
        eventsQueue.push({ eventType, args })
      }
    }
  }
)

function handleEvent(eventType, args) {
  try {
    const { sendRaw } = initOptions.serverConfig.events[eventType]
    if (sendRaw) {
      sendEvent(eventType, args, true)
    }
  } catch (e) { }

  switch (eventType) {
    case AUCTION_INIT:
      onAuctionInit(args)
      break
    case BID_REQUESTED:
      onBidRequested(args)
      break
    case NO_BID:
      onNoBid(args)
      break
    case BID_RESPONSE:
      onBidResponse(args)
      break
    case BID_WON:
      onBidWon(args)
      break
    case BID_TIMEOUT:
      onBidTimeout(args)
      break
    case AUCTION_END:
      onAuctionEnd(args)
      break
    case AD_RENDER_FAILED:
      onAdRenderFailed(args)
      break
  }
}

function sendEvent(eventType, args, isRaw) {
  let data = deepClone(args)

  Object.assign(data, buildCommonDataProperties(), {
    eventType
  })

  if (isRaw) {
    let rawEndpoint
    try {
      const { endpoint, omitRawFields } = initOptions.serverConfig.events[eventType]
      rawEndpoint = endpoint
      handleCustomRawFields(data, omitRawFields)
    } catch (e) { }
    ajaxCall(rawEndpoint || ENDPOINTS.RAW, () => { }, JSON.stringify(data))
  } else {
    let endpoint
    if (eventType === SERVER_EVENTS.AD_RENDER_FAILED) {
      endpoint = ENDPOINTS.AD_RENDER_FAILED
    } else if (eventType === SERVER_EVENTS.WON) {
      endpoint = ENDPOINTS.BID_WON
    } else {
      endpoint = ENDPOINTS.AUCTION
    }

    ajaxCall(endpoint, () => { }, JSON.stringify(data))
  }
}

function checkEventsQueue() {
  while (eventsQueue.length) {
    const event = eventsQueue.shift()
    handleEvent(event.eventType, event.args)
  }
}

function buildAuctionData(auction) {
  const auctionData = deepClone(auction)
  const keysToRemove = ['adUnitCodes', 'auctionStatus', 'bidderRequests', 'bidsReceived', 'noBids', 'winningBids', 'timestamp', 'config']

  keysToRemove.forEach(key => {
    delete auctionData[key]
  })

  handleCustomFields(auctionData, AUCTION_END, auction)

  // turn bids object into array of objects
  Object.keys(auctionData.adUnits).forEach(adUnit => {
    const adUnitObj = auctionData.adUnits[adUnit]
    adUnitObj.bids = Object.keys(adUnitObj.bids).map(key => mapBid(adUnitObj.bids[key]))
    delete adUnitObj['adUnitCode']
    delete adUnitObj['code']
    delete adUnitObj['transactionId']
  })

  // turn adUnits objects into array of adUnits
  auctionData.adUnits = Object.keys(auctionData.adUnits).map(key => auctionData.adUnits[key])

  return auctionData
}

function buildCommonDataProperties() {
  return {
    pvid: PAGEVIEW_ID,
    pid: initOptions.pid,
    pbModuleVersion
  }
}

function buildLogMessage(message) {
  return `oolo: ${message}`
}

function parseBidId(bid) {
  return bid.bidId || bid.requestId
}

function mapBid({
  bidStatus,
  start,
  end,
  mediaType,
  creativeId,
  originalCpm,
  originalCurrency,
  source,
  netRevenue,
  currency,
  width,
  height,
  timeToRespond,
  responseTimestamp,
  ...rest
}, eventType) {
  const bidObj = {
    bst: bidStatus,
    s: start,
    e: responseTimestamp || end,
    mt: mediaType,
    crId: creativeId,
    oCpm: originalCpm,
    oCur: originalCurrency,
    src: source,
    nrv: netRevenue,
    cur: currency,
    w: width,
    h: height,
    ttr: timeToRespond,
    ...rest,
  }

  delete bidObj['bidRequestsCount']
  delete bidObj['bidderRequestId']
  delete bidObj['bidderRequestsCount']
  delete bidObj['bidderWinsCount']
  delete bidObj['schain']
  delete bidObj['refererInfo']
  delete bidObj['statusMessage']
  delete bidObj['status']
  delete bidObj['adUrl']
  delete bidObj['ad']
  delete bidObj['usesGenericKeys']
  delete bidObj['requestTimestamp']

  try {
    handleCustomFields(bidObj, eventType || BID_RESPONSE, rest)
  } catch (e) { }

  return bidObj
}

function handleCustomFields(obj, eventType, args) {
  try {
    const { pickFields, omitFields } = initOptions.serverConfig.events[eventType]

    if (pickFields && obj && args) {
      Object.assign(obj, pick(args, pickFields))
    }

    if (omitFields && obj && args) {
      omitFields.forEach(field => {
        deepSetValue(obj, field, undefined)
      })
    }
  } catch (e) { }
}

function handleCustomRawFields(obj, omitRawFields) {
  try {
    if (omitRawFields && obj) {
      omitRawFields.forEach(field => {
        deepSetValue(obj, field, undefined)
      })
    }
  } catch (e) { }
}

function getServerConfig() {
  const defaultConfig = { events: {} }

  ajaxCall(
    ENDPOINTS.CONFIG + '?pid=' + initOptions.pid,
    {
      success: function (data) {
        try {
          initOptions.serverConfig = JSON.parse(data) || defaultConfig
        } catch (e) {
          initOptions.serverConfig = defaultConfig
        }
        checkEventsQueue()
      },
      error: function () {
        initOptions.serverConfig = defaultConfig
        checkEventsQueue()
      }
    },
    null
  )
}

function sendPage() {
  setTimeout(() => {
    const payload = {
      timestamp: Date.now(),
      screenWidth: window.screen.width,
      screenHeight: window.screen.height,
      url: window.location.href,
      protocol: window.location.protocol,
      origin: getOrigin(),
      referrer: getTopWindowReferrer(),
      pbVersion: prebidVersion,
    }

    Object.assign(payload, buildCommonDataProperties(), getPagePerformance())
    ajaxCall(ENDPOINTS.PAGE_DATA, () => { }, JSON.stringify(payload))
  }, 0)
}

function sendHbConfigData() {
  const conf = {}
  const pbjsConfig = config.getConfig()
  // Check if pbjsConfig.userSync exists and has userIds property
  if (pbjsConfig.userSync && pbjsConfig.userSync.userIds) {
    // Delete the userIds property
    delete pbjsConfig.userSync.userIds;
  }

  Object.keys(pbjsConfig).forEach(key => {
    if (key[0] !== '_') {
      conf[key] = pbjsConfig[key]
    }
  })

  ajaxCall(ENDPOINTS.HBCONFIG, () => { }, JSON.stringify(conf))
}

function getPagePerformance() {
  let timing

  try {
    timing = window.top.performance.timing
  } catch (e) { }

  if (!timing) {
    return
  }

  const { navigationStart, domContentLoadedEventEnd, loadEventEnd } = timing
  const domContentLoadTime = domContentLoadedEventEnd - navigationStart
  const pageLoadTime = loadEventEnd - navigationStart

  return {
    domContentLoadTime,
    pageLoadTime,
  }
}

function getTopWindowReferrer() {
  try {
    return window.top.document.referrer
  } catch (e) {
    return ''
  }
}

function generatePageViewId(min = 10000, max = 90000) {
  var randomNumber = Math.floor((Math.random() * max) + min)
  var currentdate = new Date()
  var currentTime = {
    getDate: currentdate.getDate(),
    getMonth: currentdate.getMonth(),
    getFullYear: currentdate.getFullYear(),
    getHours: currentdate.getHours(),
    getMinutes: currentdate.getMinutes(),
    getSeconds: currentdate.getSeconds(),
    getMilliseconds: currentdate.getMilliseconds()
  }
  return ((currentTime.getDate <= 9) ? '0' + (currentTime.getDate) : (currentTime.getDate)) + '' +
    (currentTime.getMonth + 1 <= 9 ? '0' + (currentTime.getMonth + 1) : (currentTime.getMonth + 1)) + '' +
    currentTime.getFullYear % 100 + '' +
    (currentTime.getHours <= 9 ? '0' + currentTime.getHours : currentTime.getHours) + '' +
    (currentTime.getMinutes <= 9 ? '0' + currentTime.getMinutes : currentTime.getMinutes) + '' +
    (currentTime.getSeconds <= 9 ? '0' + currentTime.getSeconds : currentTime.getSeconds) + '' +
    (currentTime.getMilliseconds % 100 <= 9 ? '0' + (currentTime.getMilliseconds % 100) : (currentTime.getMilliseconds % 100)) + '' +
    (randomNumber)
}

function ajaxCall(endpoint, callback, data, options = {}) {
  if (data) {
    options.contentType = 'application/json'
  }

  return ajax(endpoint, callback, data, options)
}

ooloAdapter.originEnableAnalytics = ooloAdapter.enableAnalytics
ooloAdapter.enableAnalytics = function (config) {
  ooloAdapter.originEnableAnalytics(config)
  initOptions = config ? config.options : {}

  if (!initOptions.pid) {
    logError(buildLogMessage('enableAnalytics missing config object with "pid"'))
    return
  }

  getServerConfig()
  sendHbConfigData()

  if (document.readyState === 'complete') {
    sendPage()
  } else {
    window.addEventListener('load', sendPage)
  }

  logInfo(buildLogMessage('enabled analytics adapter'), config)
  ooloAdapter.enableAnalytics = function () {
    logInfo(buildLogMessage('Analytics adapter already enabled..'))
  }
}

ooloAdapter.originDisableAnalytics = ooloAdapter.disableAnalytics
ooloAdapter.disableAnalytics = function () {
  auctions = {}
  initOptions = {}
  ooloAdapter.originDisableAnalytics()
}

adapterManager.registerAnalyticsAdapter({
  adapter: ooloAdapter,
  code: ADAPTER_CODE
})

// export for testing
export {
  buildAuctionData,
  generatePageViewId
}

export default ooloAdapter