Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: break dwon the Advertising.js #98

Closed
PoChenKuo opened this issue May 17, 2023 · 4 comments
Closed

chore: break dwon the Advertising.js #98

PoChenKuo opened this issue May 17, 2023 · 4 comments
Assignees

Comments

@PoChenKuo
Copy link

PoChenKuo commented May 17, 2023

const EXECUTE_PLUGINS_ACTION = {
  SETUP: 'setup',
  DISPLAYSLOTS: 'displaySlots',
  DISPLAYOUTOFPAGESLOT: 'displayOutOfPageSlot',
  REFRESHINTERSTITIALSLOT: 'refreshInterstitialSlot',
  SETUPPREBID: 'setupPrebid',
  TEARDOWNPREBID: 'teardownPrebid',
  SETUPGPT: 'setupGpt',
  TEARDOWNGPT: 'teardownGpt',
}

export default class Advertising {
  constructor(config, plugins = [], onError = () => { }) {
    this.config = config
    this.slots = {}
    this.outOfPageSlots = {}
    this.plugins = plugins
    this.onError = onError
    this.gptSizeMappings = {}
    this.customEventCallbacks = {}
    this.customEventHandlers = {}
    this.queue = []
    this.setDefaultConfig()
    this.defaultLazyLoadConfig = {
      marginPercent: 0,
      mobileScaling: 1,
    }

    this.requestManager = {
      aps: false,
      prebid: false,
    }
  }


  // ---------- PUBLIC METHODS ----------

  async setup() {
    this.isPrebidUsed =
      typeof this.config.usePrebid === 'undefined' ? typeof window.pbjs !== 'undefined' : this.config.usePrebid
    this.isAPSUsed =
      typeof this.config.useAPS === 'undefined' ? typeof window.apstag !== 'undefined' : this.config.useAPS
    this.executePlugins(EXECUTE_PLUGINS_ACTION.SETUP)
    const { slots, outOfPageSlots, queue, isPrebidUsed, isAPSUsed } = this
    this.setupCustomEvents()
    const setUpQueueItems = [Advertising.queueForGPT(this.setupGpt.bind(this), this.onError)]
    if (isAPSUsed) {
      this.initApstag()
    }
    if (isPrebidUsed) {
      setUpQueueItems.push(Advertising.queueForPrebid(this.setupPrebid.bind(this), this.onError))
    }

    await Promise.all(setUpQueueItems)
    if (queue.length === 0) {
      return
    }
    this.setCustomEventCallbackByQueue(queue)
    const { divIds, selectedSlots } = getDivIdsAndSlots(queue, outOfPageSlots, slots)

    if (isPrebidUsed) {
      Advertising.queueForPrebid(this.getPbjFetchBidsCallback(divIds, selectedSlots), this.onError)
    }

    if (this.isAPSUsed) {
      this.apstagFetchBids(selectedSlots, selectedSlots)
    }

    if (!isPrebidUsed && !isAPSUsed) {
      Advertising.queueForGPT(() => window.googletag.pubads().refresh(selectedSlots), this.onError)
    }
  }

  async teardown() {
    this.teardownCustomEvents()
    const teardownQueueItems = [Advertising.queueForGPT(this.teardownGpt.bind(this), this.onError)]
    if (this.isPrebidUsed) {
      teardownQueueItems.push(Advertising.queueForPrebid(this.teardownPrebid.bind(this), this.onError))
    }
    await Promise.all(teardownQueueItems)
    this.slots = {}
    this.gptSizeMappings = {}
    this.queue = []
  }

  activate(id, customEventHandlers = {}) {
    const { slots, isPrebidUsed } = this
    // check if have slots from configurations
    if (Object.values(slots).length === 0 && id) {
      this.queue.push({ id, customEventHandlers })
      return
    }
    this.setCustomEventCallback(id, customEventHandlers)

    if (isPrebidUsed) {
      Advertising.queueForPrebid(this.getPbjFetchBidsCallback([id], [slots[id].gpt]), this.onError)
    }

    if (this.isAPSUsed) {
      this.apstagFetchBids([slots[id]], [slots[id].gpt]);
    }

    if (!this.isPrebidUsed && !this.isAPSUsed) {
      Advertising.queueForGPT(() => window.googletag.pubads().refresh([slots[id].gpt]), this.onError)
    }
  }

  isConfigReady() {
    return Boolean(this.config)
  }

  setConfig(config) {
    this.config = config
    this.setDefaultConfig()
  }

  // ---------- PRIVATE METHODS ----------
  apstagFetchBids(selectedSlots, checkedSlots) {
    try {
      window.apstag.fetchBids(
        {
          slots: selectedSlots.map((slot) => slot.aps),
        },
        () => {
          Advertising.queueForGPT(() => {
            window.apstag.setDisplayBids()
            this.requestManager.aps = true // signals that APS request has completed
            this.refreshSlots(checkedSlots) // checks whether both APS and Prebid have returned
          }, this.onError)
        }
      )
    } catch (error) {
      this.onError(error)
    }
  }

  getPbjFetchBidsCallback(divIds, selectedSlots) {
    return () =>
      window.pbjs.requestBids({
        adUnitCodes: divIds,
        bidsBackHandler: () => {
          window.pbjs.setTargetingForGPTAsync(divIds)
          this.requestManager.prebid = true
          this.refreshSlots(selectedSlots)
        },
      })
  }

  getDivIdsAndSlots(queue, outOfPageSlots, slots) {
    const divIds = []
    const selectedSlots = []
    queue.forEach(({ id }) => {
      if (id) {
        divIds.push(id)
      }
      selectedSlots.push(slots[id]?.gpt || outOfPageSlots[id])
    })
    return { divIds, selectedSlots }
  }

  setCustomEventCallbackByQueue(queue) {
    for (let i = 0; i < queue.length; i++) {
      const { id, customEventHandlers } = queue[i]
      this.setCustomEventCallback(id, customEventHandlers)
    }
  }
  setCustomEventCallback(id, customEventHandlers) {
    Object.keys(customEventHandlers).forEach((customEventId) => {
      if (!this.customEventCallbacks[customEventId]) {
        this.customEventCallbacks[customEventId] = {}
      }
      this.customEventCallbacks[customEventId][id] = customEventHandlers[customEventId]
    })
  }

  initApstag() {
    try {
      window.apstag.init({
        ...this.config.aps,
        adServer: 'googletag',
      })
    } catch (error) {
      this.onError(error)
    }
  }

  setupCustomEvents() {
    if (!this.config.customEvents) {
      return
    }
    Object.keys(this.config.customEvents).forEach((customEventId) =>
      this.setupCustomEvent(customEventId, this.config.customEvents[customEventId])
    )
  }

  setupCustomEvent(customEventId, { eventMessagePrefix, divIdPrefix }) {
    const { customEventCallbacks } = this
    this.customEventHandlers[customEventId] = ({ data }) => {
      if (typeof data !== 'string' || !data.startsWith(`${eventMessagePrefix}`)) {
        return
      }
      const divId = `${divIdPrefix || ''}${data.substr(eventMessagePrefix.length)}`
      const callbacks = customEventCallbacks[customEventId]
      if (!callbacks) {
        return
      }
      const callback = callbacks[divId]
      if (callback) {
        callback()
      }
    }
    window.addEventListener('message', this.customEventHandlers[customEventId])
  }

  teardownCustomEvents() {
    if (!this.config.customEvents) {
      return
    }
    Object.keys(this.config.customEvents).forEach((customEventId) =>
      window.removeEventListener('message', this.customEventHandlers[customEventId])
    )
  }

  defineGptSizeMappings() {
    if (!this.config.sizeMappings) {
      return
    }
    const entries = Object.entries(this.config.sizeMappings)
    for (let i = 0; i < entries.length; i++) {
      const [key, value] = entries[i]
      const sizeMapping = window.googletag.sizeMapping()
      for (let q = 0; q < value.length; q++) {
        const { viewPortSize, sizes } = value[q]
        sizeMapping.addSize(viewPortSize, sizes)
      }
      this.gptSizeMappings[key] = sizeMapping.build()
    }
  }

  getGptSizeMapping(sizeMappingName) {
    return sizeMappingName && this.gptSizeMappings[sizeMappingName] ? this.gptSizeMappings[sizeMappingName] : null
  }

  defineSlots() {
    if (!this.config.slots) {
      return
    }
    this.config.slots.forEach(({ id, path, collapseEmptyDiv, targeting = {}, sizes, sizeMappingName }) => {
      const gptSlot = window.googletag.defineSlot(path || this.config.path, sizes, id)

      const sizeMapping = this.getGptSizeMapping(sizeMappingName)
      if (sizeMapping) {
        gptSlot.defineSizeMapping(sizeMapping)
      }

      if (collapseEmptyDiv && collapseEmptyDiv.length && collapseEmptyDiv.length > 0) {
        gptSlot.setCollapseEmptyDiv(...collapseEmptyDiv)
      }

      const entries = Object.entries(targeting)
      for (let i = 0; i < entries.length; i++) {
        const [key, value] = entries[i]
        gptSlot.setTargeting(key, value)
      }

      gptSlot.addService(window.googletag.pubads())

      const apsSlot = {
        slotID: id,
        slotName: path,
        sizes: sizes.filter(
          // APS requires sizes to have type number[][]. Each entry in sizes
          // should be an array containing height and width.
          (size) => typeof size === 'object' && typeof size[0] === 'number' && typeof size[1] === 'number'
        ),
      }

      this.slots[id] = { gpt: gptSlot, aps: apsSlot }
    })
  }

  defineOutOfPageSlots() {
    if (this.config.outOfPageSlots) {
      this.config.outOfPageSlots.forEach(({ id, path }) => {
        const slot = window.googletag.defineOutOfPageSlot(path || this.config.path, id)
        slot.addService(window.googletag.pubads())
        this.outOfPageSlots[id] = slot
      })
    }
  }

  defineInterstitialSlot() {
    if (this.config.interstitialSlot) {
      const { path, targeting } = this.config.interstitialSlot
      const slot = window.googletag.defineOutOfPageSlot(
        path || this.config.path,
        window.googletag.enums.OutOfPageFormat.INTERSTITIAL
      )
      if (slot) {
        const entries = Object.entries(targeting || [])
        for (let i = 0; i < entries.length; i++) {
          const [key, value] = entries[i]
          slot.setTargeting(key, value)
        }
        slot.addService(window.googletag.pubads())
        this.interstitialSlot = slot
      }
    }
  }

  displaySlots() {
    this.executePlugins(EXECUTE_PLUGINS_ACTION.DISPLAYSLOTS)
    this.config.slots.forEach(({ id }) => {
      window.googletag.display(id)
    })
  }

  displayOutOfPageSlots() {
    this.executePlugins(EXECUTE_PLUGINS_ACTION.DISPLAYOUTOFPAGESLOT)
    if (this.config.outOfPageSlots) {
      this.config.outOfPageSlots.forEach(({ id }) => {
        window.googletag.display(id)
      })
    }
  }

  refreshInterstitialSlot() {
    this.executePlugins(EXECUTE_PLUGINS_ACTION.REFRESHINTERSTITIALSLOT)
    if (this.interstitialSlot) {
      window.googletag.pubads().refresh([this.interstitialSlot])
    }
  }

  getAdUnits(slots) {
    return slots.reduce(
      (acc, currSlot) =>
        acc.concat(
          currSlot.prebid.map((currPrebid) => ({
            code: currSlot.id,
            mediaTypes: currPrebid.mediaTypes,
            bids: currPrebid.bids,
          }))
        ),
      []
    )
  }

  setupPrebid() {
    this.executePlugins(EXECUTE_PLUGINS_ACTION.SETUPPREBID)
    const adUnits = this.getAdUnits(this.config.slots)
    window.pbjs.addAdUnits(adUnits)
    window.pbjs.setConfig(this.config.prebid)
  }

  teardownPrebid() {
    this.executePlugins(EXECUTE_PLUGINS_ACTION.TEARDOWNPREBID)
    this.getAdUnits(this.config.slots).forEach(({ code }) => window.pbjs.removeAdUnit(code))
  }

  setupGpt() {
    this.executePlugins(EXECUTE_PLUGINS_ACTION.SETUPGPT)
    const pubads = window.googletag.pubads()
    const { targeting } = this.config
    this.defineGptSizeMappings()
    this.defineSlots()
    this.defineOutOfPageSlots()
    this.defineInterstitialSlot()
    const entries = Object.entries(targeting)
    for (let i = 0; i < entries.length; i++) {
      const [key, value] = entries[i]
      pubads.setTargeting(key, value)
    }
    pubads.disableInitialLoad()
    pubads.enableSingleRequest()

    window.googletag.enableServices()
    this.displaySlots()
    this.displayOutOfPageSlots()
    this.refreshInterstitialSlot()
  }

  teardownGpt() {
    this.executePlugins(EXECUTE_PLUGINS_ACTION.TEARDOWNGPT)
    window.googletag.destroySlots()
  }

  setDefaultConfig() {
    if (!this.config) {
      return
    }
    if (!this.config.prebid) {
      this.config.prebid = {}
    }
    if (!this.config.metaData) {
      this.config.metaData = {}
    }
    if (!this.config.targeting) {
      this.config.targeting = {}
    }
    if (this.config.enableLazyLoad === true) {
      this.config.enableLazyLoad = this.defaultLazyLoadConfig
    }
    if (this.config.slots) {
      this.config.slots = this.config.slots.map((slot) =>
        slot.enableLazyLoad === true ? { ...slot, enableLazyLoad: this.defaultLazyLoadConfig } : slot
      )
    }
  }

  executePlugins(method) {
    for (let i = 0; i < this.plugins.length; i++) {
      const func = this.plugins[i][method]
      if (func) {
        func.call(this)
      }
    }
  }

  // when both APS and Prebid have returned, initiate ad request
  refreshSlots(selectedSlots) {
    // If using APS, we need to check that we got a bid from APS.
    // If using Prebid, we need to check that we got a bid from Prebid.
    if (this.isAPSUsed !== this.requestManager.aps || this.isPrebidUsed !== this.requestManager.prebid) {
      return
    }

    Advertising.queueForGPT(() => {
      window.googletag.pubads().refresh(selectedSlots)
    }, this.onError)

    this.requestManager.aps = false
    this.requestManager.prebid = false
  }

  static queueForGPT(func, onError) {
    return Advertising.withQueue(window.googletag.cmd, func, onError)
  }

  static queueForPrebid(func, onError) {
    return Advertising.withQueue(window.pbjs.que, func, onError)
  }

  static withQueue(queue, func, onError) {
    return new Promise((resolve) =>
      queue.push(() => {
        try {
          func()
          resolve()
        } catch (error) {
          onError(error)
        }
      })
    )
  }
}
@PoChenKuo
Copy link
Author

@HelloAlexPan

  • I have not verify if it works or not.

@HelloAlexPan
Copy link
Member

Thanks for reporting this!

@DanSnow
Copy link
Contributor

DanSnow commented May 19, 2023

@PoChenKuo Would you like to submit an improvement for Advertising.js? If so, could you please submit it as a pull request so that we can review it?

@DanSnow
Copy link
Contributor

DanSnow commented May 22, 2023

moved to #104

@DanSnow DanSnow closed this as not planned Won't fix, can't repro, duplicate, stale May 22, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants