From 1bfabf323c14c7a82862858dd91c87cc1ef7bd7c Mon Sep 17 00:00:00 2001 From: Saigredan Date: Tue, 23 Mar 2021 13:23:45 +0100 Subject: [PATCH] YIEL-3463: Add next GPT features --- CHANGELOG.md | 3 + README.md | 12 +++ example/src/App.tsx | 12 ++- src/Components/AdManagerSlot.test.tsx | 16 ++-- src/Context/AdManagerProvider.test.tsx | 2 +- src/Context/AdManagerProvider.tsx | 36 +++++++- src/Utils/AdManager.ts | 87 ++++++++----------- src/Utils/AdManagerUtils/createSlot.ts | 20 +++++ src/Utils/AdManagerUtils/index.ts | 6 ++ .../initializeGlobalGPTOptions.ts | 62 +++++++++++++ src/Utils/AdManagerUtils/setSizeMapping.ts | 14 +++ src/Utils/AdManagerUtils/setTargeting.ts | 10 +++ 12 files changed, 219 insertions(+), 61 deletions(-) create mode 100644 src/Utils/AdManagerUtils/createSlot.ts create mode 100644 src/Utils/AdManagerUtils/index.ts create mode 100644 src/Utils/AdManagerUtils/initializeGlobalGPTOptions.ts create mode 100644 src/Utils/AdManagerUtils/setSizeMapping.ts create mode 100644 src/Utils/AdManagerUtils/setTargeting.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 675f06f..84a4778 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,9 @@ In addition to above spec use `Action` group to gather all needed actions requir ## Unreleased +### Added +- Extend GPT supported options + ## [1.1.0] - 2021-01-12 Production ### Added diff --git a/README.md b/README.md index 6998cb7..dd02a81 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,18 @@ return ( ``` `AdManagerProvider` context component should be placed at the top of your React app. It is responsible for injecting GPT and Yieldbird Wrapper scripts, initializing variables and storing helper data. +| name | type | required | description | +| :---- | :----: | :----: | :---- | +| `collapseEmptyDivs` | boolean | false | Google AdManager collapseEmptyDivs option | +| `globalTargeting` | object | false | targeting object which can be used to pass aditional key-values pairs to pubads object | +| `uuid` | string | true | Yieldbird UUID required to load Wrapper script | +| `onImpressionViewable` | function | false | Callback function for 'impressionViewable' event | +| `onSlotOnload` | function | false | Callback function for 'slotOnload' event | +| `onSlotRender` | function | false | Callback function for 'slotRenderEnded' event | +| `onSlotRequested` | function | false | Callback function for 'slotRequested' event | +| `onSlotResponseReceived` | function | false | Callback function for 'slotResponseReceived' event | +| `onSlotVisibilityChanged` | function | false | Callback function for 'slotVisibilityChanged' event | + `AdManagerSlot` is a simple ad component, with properties similar to GPT slot. It is responsible for rendering ad in specified place. You can use it with following properties: | name | type | required | description | diff --git a/example/src/App.tsx b/example/src/App.tsx index 5d8c103..8161f6f 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -10,7 +10,17 @@ const App = () => { }, [toggle, setToggle]) return ( - + { console.log('impresion viewable!', event) }} + onSlotRequested={(event) => { console.log('slot requested!', event) }} + >
AD 1 diff --git a/src/Components/AdManagerSlot.test.tsx b/src/Components/AdManagerSlot.test.tsx index 3f6c8d6..8e5aa71 100644 --- a/src/Components/AdManagerSlot.test.tsx +++ b/src/Components/AdManagerSlot.test.tsx @@ -77,7 +77,7 @@ describe('AdManagerSlot', () => { }) expect(wrapper.find('#foo')).toBeTruthy() - expect(window.googletag.cmd).toHaveLength(1) + expect(window.googletag.cmd).toHaveLength(2) expect(window.Yieldbird.cmd).toHaveLength(1) }) @@ -94,6 +94,7 @@ describe('AdManagerSlot', () => { // create slot commands window.Yieldbird.cmd[0]() window.googletag.cmd[1]() + window.googletag.cmd[2]() expect(window.googletag.enableServices).toHaveBeenCalledTimes(1) expect(window.googletag.defineSlot).toHaveBeenCalledTimes(1) @@ -101,12 +102,13 @@ describe('AdManagerSlot', () => { expect(window.Yieldbird.setGPTTargeting).toHaveBeenCalledTimes(1) expect(window.Yieldbird.cmd).toHaveLength(1) - expect(window.googletag.cmd).toHaveLength(2) + expect(window.googletag.cmd).toHaveLength(3) await wrapper.find('button').simulate('click') - expect(window.googletag.cmd).toHaveLength(3) + expect(window.googletag.cmd).toHaveLength(5) // destroy slot command - window.googletag.cmd[2]() + window.googletag.cmd[3]() + window.googletag.cmd[4]() expect(window.googletag.destroySlots).toHaveBeenCalledTimes(1) await wrapper.find('button').simulate('click') @@ -114,7 +116,8 @@ describe('AdManagerSlot', () => { // create slot command again expect(window.Yieldbird.cmd).toHaveLength(2) window.Yieldbird.cmd[1]() - window.googletag.cmd[3]() + window.googletag.cmd[5]() + window.googletag.cmd[6]() expect(window.googletag.enableServices).toHaveBeenCalledTimes(2) expect(window.googletag.defineSlot).toHaveBeenCalledTimes(2) @@ -209,6 +212,7 @@ describe('AdManagerSlot', () => { // create slot commands window.Yieldbird.cmd[0]() window.googletag.cmd[1]() + window.googletag.cmd[2]() expect(window.googletag.enableServices).toHaveBeenCalledTimes(1) expect(window.googletag.defineSlot).toHaveBeenCalledTimes(1) @@ -217,7 +221,7 @@ describe('AdManagerSlot', () => { await jest.advanceTimersByTime(5000) jest.runAllTimers() - window.googletag.cmd[2]() + window.googletag.cmd[3]() expect(window.googletag.display).toHaveBeenCalledTimes(1) expect(global.IntersectionObserver.prototype.unobserve).toHaveBeenCalled() }) diff --git a/src/Context/AdManagerProvider.test.tsx b/src/Context/AdManagerProvider.test.tsx index ba7767d..8965ddd 100644 --- a/src/Context/AdManagerProvider.test.tsx +++ b/src/Context/AdManagerProvider.test.tsx @@ -13,6 +13,6 @@ describe('AdManagerSlot', () => { }) expect(window.Yieldbird?.cmd).toHaveLength(0) - expect(window.googletag?.cmd).toHaveLength(1) + expect(window.googletag?.cmd).toHaveLength(2) }) }) diff --git a/src/Context/AdManagerProvider.tsx b/src/Context/AdManagerProvider.tsx index 6bb18f5..54a4fe0 100644 --- a/src/Context/AdManagerProvider.tsx +++ b/src/Context/AdManagerProvider.tsx @@ -6,8 +6,22 @@ import { initializeAdStack } from '../Utils/headerScripts' import { isIntersectionObserverAvailable } from '../Utils/intersectionObserver' interface Props { + collapseEmptyDivs?: boolean + globalTargeting?: Record uuid: string refreshDelay?: number + onImpressionViewable?: ( + event: googletag.events.ImpressionViewableEvent + ) => void + onSlotOnload?: (event: googletag.events.SlotOnloadEvent) => void + onSlotRender?: (event: googletag.events.SlotRenderEndedEvent) => void + onSlotRequested?: (event: googletag.events.SlotRequestedEvent) => void + onSlotResponseReceived?: ( + event: googletag.events.SlotResponseReceived + ) => void + onSlotVisibilityChanged?: ( + event: googletag.events.SlotVisibilityChangedEvent + ) => void } export const AdManagerContext = React.createContext({ @@ -22,11 +36,29 @@ export const AdManagerContext = React.createContext({ export const AdManagerProvider: React.FC = ({ children, uuid, - refreshDelay + refreshDelay, + collapseEmptyDivs, + globalTargeting, + onImpressionViewable, + onSlotOnload, + onSlotRender, + onSlotRequested, + onSlotResponseReceived, + onSlotVisibilityChanged }) => { const [adsMap, setAdsMap] = useState([]) - const adManager = new AdManager(refreshDelay) + const adManager = new AdManager( + collapseEmptyDivs, + globalTargeting, + refreshDelay, + onImpressionViewable, + onSlotOnload, + onSlotRender, + onSlotRequested, + onSlotResponseReceived, + onSlotVisibilityChanged + ) const intersectionObserver = isIntersectionObserverAvailable() && new IntersectionObserver((entries, observer) => { diff --git a/src/Utils/AdManager.ts b/src/Utils/AdManager.ts index b8031ac..70db327 100644 --- a/src/Utils/AdManager.ts +++ b/src/Utils/AdManager.ts @@ -1,6 +1,12 @@ import { ensureScripts } from './headerScripts' import { isIntersectionObserverAvailable } from './intersectionObserver' +import { + createSlot, + initiaizeGlobalGPTOptions, + setTargeting, + setSizeMapping +} from './AdManagerUtils' export class AdManager { private adsToRefresh: { @@ -19,7 +25,23 @@ export class AdManager { private retargetTimeout: number - constructor(timeout = 1000) { + constructor( + collapseEmptyDivs?: boolean, + globalTargeting?: Record, + timeout = 1000, + onImpressionViewable?: ( + event: googletag.events.ImpressionViewableEvent + ) => void, + onSlotOnload?: (event: googletag.events.SlotOnloadEvent) => void, + onSlotRender?: (event: googletag.events.SlotRenderEndedEvent) => void, + onSlotRequested?: (event: googletag.events.SlotRequestedEvent) => void, + onSlotResponseReceived?: ( + event: googletag.events.SlotResponseReceived + ) => void, + onSlotVisibilityChanged?: ( + event: googletag.events.SlotVisibilityChangedEvent + ) => void + ) { this.adsToRefresh = {} this.adsToRetarget = {} this.refreshInterval = null @@ -28,6 +50,16 @@ export class AdManager { this.retargetTimeout = timeout ensureScripts() + initiaizeGlobalGPTOptions( + collapseEmptyDivs, + globalTargeting, + onImpressionViewable, + onSlotOnload, + onSlotRender, + onSlotRequested, + onSlotResponseReceived, + onSlotVisibilityChanged + ) } public static defineSlot( @@ -43,10 +75,10 @@ export class AdManager { if (typeof window !== 'undefined') { window.Yieldbird.cmd.push(() => { window.googletag.cmd.push(() => { - const slot = this.createSlot(adUnitPath, size, optDiv) + const slot = createSlot(adUnitPath, size, optDiv) - this.setTargeting(slot, targeting) - this.setSizeMapping(slot, sizeMapping) + setTargeting(slot, targeting) + setSizeMapping(slot, sizeMapping) !shouldRefreshAds && window.Yieldbird.setGPTTargeting([slot]) window.googletag.enableServices() @@ -151,51 +183,4 @@ export class AdManager { } }) } - - private static createSlot( - adUnitPath: string, - size: googletag.GeneralSize, - optDiv: string - ) { - let slot = window.googletag - .pubads() - .getSlots() - .find((el) => el.getSlotElementId() === optDiv) - - slot = - slot || - window.googletag - .defineSlot(adUnitPath, size, optDiv) - .addService(window.googletag.pubads()) - - slot.clearTargeting() - - return slot - } - - private static setTargeting( - slot: googletag.Slot, - targeting?: { [key: string]: googletag.NamedSize } - ) { - if (slot && targeting) { - Object.keys(targeting).forEach((targetingKey: string) => { - slot.setTargeting(targetingKey, targeting[targetingKey]) - }) - } - } - - private static setSizeMapping( - slot: googletag.Slot, - sizeMapping?: [googletag.SingleSizeArray, googletag.GeneralSize][] - ) { - if (sizeMapping) { - const sizeMappingBuilder = window.googletag.sizeMapping() - - sizeMapping.forEach((sizeMap) => { - sizeMappingBuilder.addSize(sizeMap[0], sizeMap[1]) - }) - - slot.defineSizeMapping(sizeMappingBuilder.build()) - } - } } diff --git a/src/Utils/AdManagerUtils/createSlot.ts b/src/Utils/AdManagerUtils/createSlot.ts new file mode 100644 index 0000000..3e7ffa5 --- /dev/null +++ b/src/Utils/AdManagerUtils/createSlot.ts @@ -0,0 +1,20 @@ +export function createSlot( + adUnitPath: string, + size: googletag.GeneralSize, + optDiv: string +) { + let slot = window.googletag + .pubads() + .getSlots() + .find((el) => el.getSlotElementId() === optDiv) + + slot = + slot || + window.googletag + .defineSlot(adUnitPath, size, optDiv) + .addService(window.googletag.pubads()) + + slot.clearTargeting() + + return slot +} diff --git a/src/Utils/AdManagerUtils/index.ts b/src/Utils/AdManagerUtils/index.ts new file mode 100644 index 0000000..6192de2 --- /dev/null +++ b/src/Utils/AdManagerUtils/index.ts @@ -0,0 +1,6 @@ +import { createSlot } from './createSlot' +import { initiaizeGlobalGPTOptions } from './initializeGlobalGPTOptions' +import { setTargeting } from './setTargeting' +import { setSizeMapping } from './setSizeMapping' + +export { createSlot, initiaizeGlobalGPTOptions, setTargeting, setSizeMapping } diff --git a/src/Utils/AdManagerUtils/initializeGlobalGPTOptions.ts b/src/Utils/AdManagerUtils/initializeGlobalGPTOptions.ts new file mode 100644 index 0000000..f8aed9f --- /dev/null +++ b/src/Utils/AdManagerUtils/initializeGlobalGPTOptions.ts @@ -0,0 +1,62 @@ +export function initiaizeGlobalGPTOptions( + collapseEmptyDivs?: boolean, + globalTargeting?: Record, + onImpressionViewable?: ( + event: googletag.events.ImpressionViewableEvent + ) => void, + onSlotOnload?: (event: googletag.events.SlotOnloadEvent) => void, + onSlotRender?: (event: googletag.events.SlotRenderEndedEvent) => void, + onSlotRequested?: (event: googletag.events.SlotRequestedEvent) => void, + onSlotResponseReceived?: ( + event: googletag.events.SlotResponseReceived + ) => void, + onSlotVisibilityChanged?: ( + event: googletag.events.SlotVisibilityChangedEvent + ) => void +): void { + if (typeof window !== 'undefined') { + window.googletag.cmd.push(() => { + collapseEmptyDivs && window.googletag.pubads().collapseEmptyDivs() + + if (globalTargeting && Object.keys(globalTargeting).length) { + Object.keys(globalTargeting).forEach((key) => { + window.googletag.pubads().setTargeting(key, globalTargeting[key]) + }) + } + + if (onImpressionViewable) { + window.googletag + .pubads() + .addEventListener('impressionViewable', onImpressionViewable) + } + + if (onSlotOnload) { + window.googletag.pubads().addEventListener('slotOnload', onSlotOnload) + } + + if (onSlotRender) { + window.googletag + .pubads() + .addEventListener('slotRenderEnded', onSlotRender) + } + + if (onSlotRequested) { + window.googletag + .pubads() + .addEventListener('slotRequested', onSlotRequested) + } + + if (onSlotResponseReceived) { + window.googletag + .pubads() + .addEventListener('slotResponseReceived', onSlotResponseReceived) + } + + if (onSlotVisibilityChanged) { + window.googletag + .pubads() + .addEventListener('slotVisibilityChanged', onSlotVisibilityChanged) + } + }) + } +} diff --git a/src/Utils/AdManagerUtils/setSizeMapping.ts b/src/Utils/AdManagerUtils/setSizeMapping.ts new file mode 100644 index 0000000..a4798d7 --- /dev/null +++ b/src/Utils/AdManagerUtils/setSizeMapping.ts @@ -0,0 +1,14 @@ +export function setSizeMapping( + slot: googletag.Slot, + sizeMapping?: [googletag.SingleSizeArray, googletag.GeneralSize][] +) { + if (sizeMapping) { + const sizeMappingBuilder = window.googletag.sizeMapping() + + sizeMapping.forEach((sizeMap) => { + sizeMappingBuilder.addSize(sizeMap[0], sizeMap[1]) + }) + + slot.defineSizeMapping(sizeMappingBuilder.build()) + } +} diff --git a/src/Utils/AdManagerUtils/setTargeting.ts b/src/Utils/AdManagerUtils/setTargeting.ts new file mode 100644 index 0000000..6b9044a --- /dev/null +++ b/src/Utils/AdManagerUtils/setTargeting.ts @@ -0,0 +1,10 @@ +export function setTargeting( + slot: googletag.Slot, + targeting?: { [key: string]: googletag.NamedSize } +) { + if (slot && targeting) { + Object.keys(targeting).forEach((targetingKey: string) => { + slot.setTargeting(targetingKey, targeting[targetingKey]) + }) + } +}