Home Reference Source

src/adserver/googletag.js

import { cpexLog, cpexError, cpexWarn, cpexLogHeadline, loadScript, displayMetaData, isPrebidLoaded } from '../utils.js'

/**
 * This adapter replaces AdServer object instance registered in the main package instance
 * Doesn't support catching of custom formats from S2S, since refresh directly renders the ad without any possible step in-between
 */
export default class AdServerGoogleTag {
  constructor (main) {
    this.main = main
    this.dependenciesLoading = false
    this.adapter = 'googletag'
    this.displayed = false
    this.slots = []
  }

  /**
   * Mandatory. Initializes an adserver based on settings
   */
  load () {
    cpexLog('Adserver: GoogleTag adapter loading')
    // wrap pubadsReady into a promise
    this.pubAdsReady = new Promise(resolve => {
      const interval = setInterval(() => {
        if (window.googletag.pubadsReady) {
          clearInterval(interval)
          resolve()
        }
      }, 100)
    })
    const loadingParts = [this.pubAdsReady]
    window.googletag = window.googletag || { cmd: [] }
    if (this.main.settings.adserver.loadPrerequisites && this.dependenciesLoading !== true) {
      this.dependenciesLoading = true
      if (window.googletag && (typeof window.googletag.getVersion === 'function' || window.googletag._loadStarted_ || window.googletag._loaded_)) {
        cpexWarn('Adserver: GPT already present')
      } else {
        const gptLoading = loadScript(document, 'https://securepubads.g.doubleclick.net/tag/js/gpt.js', 'GPT')
          .then(() => { window.googletag.cmd.unshift(() => { window.googletag.pubads().disableInitialLoad() }) }) // unsure disabling of initial load
        loadingParts.push(gptLoading)
      }
      loadingParts.push(new Promise((resolve, reject) => {
        window.googletag.cmd.push(() => {
          window.googletag.pubads().disableInitialLoad()
          window.googletag.pubads().enableSingleRequest()
          window.googletag.pubads().enableAsyncRendering()
          window.googletag.enableServices()
          this.dependenciesLoading = false
          resolve()
        })
      }).catch(e => cpexError('Googletag que init failed', e)))
    }
    // Define slots
    if (this.main.settings.adserver.defineSlots) {
      window.googletag.cmd.push(() => {
        window.googletag.destroySlots() // clear previous slots, for SPA
      })
      this.slotsDefined = new Promise((resolve, reject) => {
        this.main.settings.adserver.defineSlots.forEach(slotDefinition => {
          if (document.getElementById(slotDefinition.elementId)) {
            window.googletag.cmd.push(() => {
              window.googletag.defineSlot(slotDefinition.path, slotDefinition.sizes, slotDefinition.elementId).addService(window.googletag.pubads())
            })
          } else {
            cpexLog(slotDefinition.elementId + ' not found in the page, probably intended')
          }
        })
        window.googletag.cmd.push(() => { resolve() })
      })
      loadingParts.push(this.slotsDefined)
    }
    this.loading = Promise.all(loadingParts).then(() => {
      cpexLog('Adserver: Googletag adapter loaded')
    }).catch(e => cpexError('Googletag failed to load', e))
    return this.loading
  }

  /**
   * Mandatory. Returns (as a promise) an array of elementIds for the page, to be used for headerbidding
   */
  async getAdsList () {
    try {
      await this.loading
      const slots = window.googletag.pubads().getSlots()
      return slots.map(slot => slot.getSlotElementId())
    } catch (e) {
      cpexError('Adserver: Failed to get ads list', e)
      return []
    }
  }

  /**
   * Mandatory. Calls the adserver to get the final ads selected and rendered
   */
  async call () {
    // Wait for DOM and GPT ready
    await this.loading
    if (this.main.debugMode) { this.logSlotTable() }

    // Remove previous listener
    if (this.eventHandler) { window.googletag.pubads().removeEventListener('slotRenderEnded', this.eventHandler) }

    window.googletag.pubads().getSlots().forEach(slot => {
      const elementId = slot.getSlotElementId()
      // Call display to initiate the slot. 2DO: figure out how to check if it's already called by the publisher
      if (this.displayed === false) { window.googletag.display(elementId) }
      // Enrich with winning bid from HB. Controlled alternative to pbjs.setTargetingForGPTAsync()
      if (this.main.headerbidding && isPrebidLoaded()) { this.addBid(elementId, slot) }
      // Save slot as regular ad (since we can't catch custom formats from direct campaigns with GAM). Custom ads from HB will be re-registered from formats
      this.main.regularAds[elementId] = { element: document.getElementById(elementId), slot }
      // Save slot by path names, to be used to reRenders
      this.slots[slot.getAdUnitPath()] = slot
    })

    // This listener will be called when a slot has finished rendering. Previous events dont have the creative info
    this.eventHandler = (event) => this.adRenderDebug(event)
    window.googletag.pubads().addEventListener('slotRenderEnded', this.eventHandler)

    // Request ads from ad server
    cpexLog('Adserver: GoogleTag display/refresh called')
    // window.googletag.pubads().updateCorrelator() // better reset, if needed
    window.googletag.pubads().refresh()
    this.displayed = true
  }

  /**
   * Mandatory. Returns DOM element id for the adUnit/hbKey
   */
  async getElementId (hbKey) {
    const adsList = await this.getAdsList()
    return adsList.includes(hbKey) ? hbKey : null
  }

  /**
   * Refresh specific to googletag, able to refresh only certain ad positions.
   * adUnits - optional array of adUnit codes
   */
  refresh (adUnits) {
    if (adUnits && adUnits.length > 0) {
      cpexLog('Adserver: GoogleTag adapter refresh called for adUnits: ', adUnits)
      // If adUnits are specified, only refresh those
      adUnits.forEach(adUnit => {
        const slot = this.slots[adUnit]
        if (slot) {
          window.googletag.pubads().refresh([slot])
        }
      })
    } else {
      // Otherwise refresh all adUnits
      cpexLog('Adserver: GoogleTag adapter refresh called for all adUnits')
      window.googletag.pubads().refresh()
    }
  }

  /****************************************************************************/
  /* SPECIFIC methods, to this adapter only                                   */
  /****************************************************************************/

  /**
   * Triggered after the render, it waits a moment for the ad to be rendered, then draws debug tags over it
   */
  adRenderDebug (event) {
    if (this.main.debugMode) {
      const elementId = event.slot.getSlotElementId()
      cpexLog('AdServer: googletag rendered into elementId ' + elementId, event)
      setTimeout(() => { this.prepareMetaData(elementId, event) }, 1000)
    }
  }

  /**
   * Add winning bid to the ad service, so it sends it to the ad server
   */
  addBid (elementId, slot) {
    const winningBid = window.pbjs.getHighestCpmBids(elementId)[0]
    if (winningBid) {
      slot.setTargeting('hb_pb_' + winningBid.bidder, winningBid.adserverTargeting.hb_pb.toString()) // hb_pb_%bidder%=%bidTier%
      if (this.main.settings.publisher.code !== 'eco') {
        slot.setTargeting('pos', slot.getAdUnitPath()) // eg. /22631723832/playground_rectangle-1
      }
      // Add AB key
      if (this.main.ab.key && this.main.ab.group) {
        slot.setTargeting(this.main.ab.key, this.main.ab.group)
      }
    }
  }

  /**
   * Wraps HB reRender to be usable with ad manager`s "path" instead of elementId. Triggered from HB service creative in GAM
   */
  gamReRender (slotPath) {
    /* this fails in a special case where path contains a 'child gam instance id': https://cpexcz.atlassian.net/browse/FED-554
    const slot = this.slots[slotPath]
    const elementId = slot.getSlotElementId()
    */
    const pathParts = slotPath.split('/')
    // if (pathParts.length <= 3) { cpexError('GAM returns only id part of path, this suggests that defineSlot names dont match. First slot will be used') }
    const slotId = pathParts[pathParts.length - 1]
    const matchingSlots = Object.keys(this.slots).filter(key => key.indexOf(slotId) !== -1)
    if (matchingSlots.length > 0) {
      const slot = this.slots[matchingSlots[0]]
      slot.fromHB = true
      const elementId = slot.getSlotElementId()
      this.main.headerbidding.reRender(elementId) // rework to slotId?
    } else {
      cpexError('Adserver: Slot not found')
    }
  }

  /**
   * Prepares an object with useful information for debubbing. Merges info from both adserver and prebid.
   * 2DO: Currently relies on the SAS flight for HB having the string "HB" in it's first comment. Should be improved.
   */
  prepareMetaData (elementId, event) {
    const creativeMetaData = { // adserver data
      adapter: this.adapter, id: elementId, size: event.size, creativeId: event.creativeId
    }
    if (this.main.customAds[elementId]) { // custom format
      creativeMetaData.customType = this.main.customAds[elementId].type
    }
    displayMetaData(elementId, creativeMetaData)
  }

  /**
   * Prints table of all found adserver slots into the console
   */
  logSlotTable () {
    const slots = window.googletag.pubads().getSlots()
    if (slots.length > 0) {
      cpexLogHeadline('Adserver: Found these GAM slots:')
      const slotTable = []
      slots.forEach(slot => {
        let sizes = ''
        slot.getSizes().forEach(size => { sizes += `[${size.width},${size.height}], ` })
        slotTable.push({ path: slot.getAdUnitPath(), element: slot.getSlotElementId(), sizes: sizes.slice(0, -2) })
      })
      console.table(slotTable)
    } else {
      cpexWarn('Adserver: No GAM slots found')
    }
  }
}