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')
}
}
}