@highfivve/ad-tag 5.10.3 → 5.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,50 @@
1
+ import { formatKey } from 'ad-tag/ads/keyValues';
2
+ import { createWaterfallContext, createOnEmptyBidRotationTrigger } from 'ad-tag/ads/auctions/waterfallContext';
3
+ export const isGamAnchor = (slot, window) => {
4
+ const [value] = slot.getTargeting(formatKey);
5
+ return (!!value &&
6
+ (value === window.googletag.enums.OutOfPageFormat.BOTTOM_ANCHOR.toString() ||
7
+ value === window.googletag.enums.OutOfPageFormat.TOP_ANCHOR.toString()));
8
+ };
9
+ const sessionStorageKeys = {
10
+ bottomMobile: 'h5v_anchor_bm',
11
+ bottomDesktop: 'h5v_anchor_bd',
12
+ top: 'h5v_anchor_t'
13
+ };
14
+ export const createAnchorContext = (configs, window__, now, logger) => {
15
+ const bottomMobile = configs.bottomMobile
16
+ ? createWaterfallContext(sessionStorageKeys.bottomMobile, configs.bottomMobile, createOnEmptyBidRotationTrigger(), window__, now, logger, 'anchor-bottom-mobile')
17
+ : undefined;
18
+ const bottomDesktop = configs.bottomDesktop
19
+ ? createWaterfallContext(sessionStorageKeys.bottomDesktop, configs.bottomDesktop, createOnEmptyBidRotationTrigger(), window__, now, logger, 'anchor-bottom-desktop')
20
+ : undefined;
21
+ const top = configs.top
22
+ ? createWaterfallContext(sessionStorageKeys.top, configs.top, createOnEmptyBidRotationTrigger(), window__, now, logger, 'anchor-top')
23
+ : undefined;
24
+ const bottomInstances = [bottomMobile, bottomDesktop].filter((instance) => !!instance);
25
+ const bottomInstanceForDomId = (domId) => {
26
+ if (configs.bottomMobile?.domId === domId) {
27
+ return bottomMobile;
28
+ }
29
+ if (configs.bottomDesktop?.domId === domId) {
30
+ return bottomDesktop;
31
+ }
32
+ return undefined;
33
+ };
34
+ return {
35
+ anchorBottomChannel: (domId) => bottomInstanceForDomId(domId)?.channel(),
36
+ anchorTopChannel: () => top?.channel(),
37
+ updateAdUnitPaths: (variables) => {
38
+ bottomInstances.forEach(instance => instance.updateAdUnitPaths(variables));
39
+ top?.updateAdUnitPaths(variables);
40
+ },
41
+ onSlotRenderEnded: (event) => {
42
+ bottomInstances.forEach(instance => instance.onSlotRenderEnded(event));
43
+ top?.onSlotRenderEnded(event);
44
+ },
45
+ onAuctionEnd: (event) => {
46
+ bottomInstances.forEach(instance => instance.onAuctionEnd(event));
47
+ top?.onAuctionEnd(event);
48
+ }
49
+ };
50
+ };
@@ -32,18 +32,18 @@ export const createRewardedAdContext = (config, window__, logger, assetLoaderSer
32
32
  };
33
33
  const attemptGam = (gamConfig) => new Promise(resolve => {
34
34
  window__.googletag.cmd.push(() => {
35
- const googletagRef = window__.googletag;
36
- const pubads = googletagRef.pubads();
35
+ const pubads = window__.googletag.pubads();
37
36
  const adUnitPath = gamAdUnitPath ?? gamConfig.adUnitPath;
38
- const slot = googletagRef.defineOutOfPageSlot(adUnitPath, googletagRef.enums.OutOfPageFormat.REWARDED);
37
+ const slot = window__.googletag.defineOutOfPageSlot(adUnitPath, window__.googletag.enums.OutOfPageFormat.REWARDED);
39
38
  if (!slot) {
40
39
  logger.error('rewardedAd', `failed to define rewarded slot for ${adUnitPath}`);
41
40
  resolve({ outcome: 'no-fill' });
42
41
  return;
43
42
  }
43
+ slot.addService(window__.googletag.pubads());
44
44
  let settled = false;
45
45
  let timeoutId;
46
- const destroySlot = () => googletagRef.destroySlots([slot]);
46
+ const destroySlot = () => window__.googletag.destroySlots([slot]);
47
47
  const removeListeners = () => {
48
48
  pubads.removeEventListener('slotRenderEnded', onSlotRenderEnded);
49
49
  pubads.removeEventListener('rewardedSlotReady', onRewardedSlotReady);
@@ -59,7 +59,7 @@ export const createRewardedAdContext = (config, window__, logger, assetLoaderSer
59
59
  resolve(attempt);
60
60
  };
61
61
  const onSlotRenderEnded = (event) => {
62
- if (event.slot !== slot) {
62
+ if (event.slot.getAdUnitPath() !== slot.getAdUnitPath()) {
63
63
  return;
64
64
  }
65
65
  if (event.isEmpty) {
@@ -70,7 +70,7 @@ export const createRewardedAdContext = (config, window__, logger, assetLoaderSer
70
70
  }
71
71
  };
72
72
  const onRewardedSlotReady = (event) => {
73
- if (event.slot !== slot) {
73
+ if (event.slot.getAdUnitPath() !== slot.getAdUnitPath()) {
74
74
  return;
75
75
  }
76
76
  window__.clearTimeout(timeoutId);
@@ -78,7 +78,7 @@ export const createRewardedAdContext = (config, window__, logger, assetLoaderSer
78
78
  event.makeRewardedVisible();
79
79
  };
80
80
  const onRewardedSlotGranted = (event) => {
81
- if (event.slot !== slot) {
81
+ if (event.slot.getAdUnitPath() !== slot.getAdUnitPath()) {
82
82
  return;
83
83
  }
84
84
  const payload = event.payload ?? defaultRewardPayload;
@@ -86,7 +86,7 @@ export const createRewardedAdContext = (config, window__, logger, assetLoaderSer
86
86
  settle({ outcome: 'granted', payload });
87
87
  };
88
88
  const onRewardedSlotClosed = (event) => {
89
- if (event.slot !== slot) {
89
+ if (event.slot.getAdUnitPath() !== slot.getAdUnitPath()) {
90
90
  return;
91
91
  }
92
92
  logger.debug('rewardedAd', 'gam rewarded slot closed');
@@ -104,8 +104,8 @@ export const createRewardedAdContext = (config, window__, logger, assetLoaderSer
104
104
  destroySlot();
105
105
  settle({ outcome: 'no-fill' });
106
106
  }, config.timeoutMs);
107
- slot.addService(pubads);
108
- googletagRef.display(slot);
107
+ window__.googletag.display(slot);
108
+ window__.googletag.pubads().refresh([slot]);
109
109
  });
110
110
  });
111
111
  const hasValidWelectToken = (welectConfig) => new Promise(resolve => {
@@ -0,0 +1,77 @@
1
+ import { resolveAdUnitPath } from 'ad-tag/ads/adUnitPath';
2
+ export const createOnEmptyBidRotationTrigger = () => ({
3
+ shouldShiftOnSlotRenderEnded: event => event.isEmpty,
4
+ shouldShiftOnAuctionEnd: (event, domId) => {
5
+ const bids = event.bidsReceived?.filter(bid => bid.adUnitCode === domId);
6
+ return (bids?.length ?? 0) === 0;
7
+ }
8
+ });
9
+ export const createWaterfallContext = (sessionStorageKey, config, rotationTrigger, window__, now, logger, logLabel) => {
10
+ const sessionStorageTimeToLive = config.ttlStorage ?? 30 * 60 * 1000;
11
+ const currentTime = now();
12
+ let resolvedAdUnitPath = config.adUnitPath;
13
+ let currentState = {
14
+ priority: config.priority,
15
+ updatedAt: currentTime
16
+ };
17
+ try {
18
+ const sessionState = window__.sessionStorage.getItem(sessionStorageKey);
19
+ if (sessionState) {
20
+ const parsedState = JSON.parse(sessionState);
21
+ if (currentTime - parsedState.updatedAt < sessionStorageTimeToLive) {
22
+ currentState = parsedState;
23
+ }
24
+ else {
25
+ currentState.updatedAt = currentTime;
26
+ }
27
+ }
28
+ }
29
+ catch (e) {
30
+ logger.error(logLabel, 'failed to load waterfall state from session storage', e);
31
+ }
32
+ if (config.priority.length === 0) {
33
+ logger.error(logLabel, 'no waterfall priority configured');
34
+ }
35
+ const persistState = () => {
36
+ currentState.updatedAt = now();
37
+ try {
38
+ window__.sessionStorage.setItem(sessionStorageKey, JSON.stringify(currentState));
39
+ }
40
+ catch (e) {
41
+ logger.error(logLabel, 'failed to persist waterfall state to session storage', e);
42
+ }
43
+ };
44
+ const shiftPriority = (arr) => {
45
+ if (arr.length === 0) {
46
+ return [];
47
+ }
48
+ return [...arr.slice(1), arr[0]];
49
+ };
50
+ const onSlotRenderEnded = (event) => {
51
+ if (event.slot.getAdUnitPath() !== resolvedAdUnitPath) {
52
+ return;
53
+ }
54
+ if (rotationTrigger.shouldShiftOnSlotRenderEnded(event)) {
55
+ currentState.priority = shiftPriority(currentState.priority);
56
+ persistState();
57
+ }
58
+ };
59
+ const onAuctionEnd = (event) => {
60
+ if (!event.adUnitCodes.includes(config.domId)) {
61
+ return;
62
+ }
63
+ if (rotationTrigger.shouldShiftOnAuctionEnd(event, config.domId)) {
64
+ currentState.priority = shiftPriority(currentState.priority);
65
+ persistState();
66
+ }
67
+ };
68
+ return {
69
+ state: () => currentState,
70
+ channel: () => currentState.priority[0],
71
+ updateAdUnitPaths: (variables) => {
72
+ resolvedAdUnitPath = resolveAdUnitPath(config.adUnitPath, variables);
73
+ },
74
+ onSlotRenderEnded,
75
+ onAuctionEnd
76
+ };
77
+ };
@@ -4,6 +4,7 @@ import { createFrequencyCapping } from './auctions/frequencyCapping';
4
4
  import { createPreviousBidCpms } from './auctions/previousBidCpms';
5
5
  import { mkConfigureStep } from './adPipeline';
6
6
  import { createInterstitialContext } from 'ad-tag/ads/auctions/interstitialContext';
7
+ import { createAnchorContext } from 'ad-tag/ads/auctions/anchorContext';
7
8
  import { createRewardedAdContext } from 'ad-tag/ads/auctions/rewardedAdContext';
8
9
  import { createTrackWinningBidder } from 'ad-tag/ads/auctions/trackWinningBidder';
9
10
  import { resolveOverridableConfig } from 'ad-tag/ads/configOverrides';
@@ -25,6 +26,9 @@ export const createGlobalAuctionContext = (window, logger, eventService, config
25
26
  const frequencyCapConfig = resolveFeature(config.frequencyCap, 'frequencyCap');
26
27
  const previousBidCpmsConfig = resolveFeature(config.previousBidCpms, 'previousBidCpms');
27
28
  const interstitialConfig = resolveFeature(config.interstitial, 'interstitial');
29
+ const anchorBottomMobileConfig = resolveFeature(config.anchorBottomMobile, 'anchorBottomMobile');
30
+ const anchorBottomDesktopConfig = resolveFeature(config.anchorBottomDesktop, 'anchorBottomDesktop');
31
+ const anchorTopConfig = resolveFeature(config.anchorTop, 'anchorTop');
28
32
  const rewardedAdConfig = resolveFeature(config.rewardedAd, 'rewardedAd');
29
33
  const trackWinningBidder = trackWinningBidderConfig?.enabled
30
34
  ? createTrackWinningBidder()
@@ -42,6 +46,16 @@ export const createGlobalAuctionContext = (window, logger, eventService, config
42
46
  const interstitial = interstitialConfig?.enabled
43
47
  ? createInterstitialContext(interstitialConfig, window, window.Date.now, logger)
44
48
  : undefined;
49
+ const anchorEnabled = !!anchorBottomMobileConfig?.enabled ||
50
+ !!anchorBottomDesktopConfig?.enabled ||
51
+ !!anchorTopConfig?.enabled;
52
+ const anchor = anchorEnabled
53
+ ? createAnchorContext({
54
+ bottomMobile: anchorBottomMobileConfig?.enabled ? anchorBottomMobileConfig : undefined,
55
+ bottomDesktop: anchorBottomDesktopConfig?.enabled ? anchorBottomDesktopConfig : undefined,
56
+ top: anchorTopConfig?.enabled ? anchorTopConfig : undefined
57
+ }, window, window.Date.now, logger)
58
+ : undefined;
45
59
  const rewardedAd = rewardedAdConfig?.enabled
46
60
  ? createRewardedAdContext(rewardedAdConfig, window, logger, assetLoaderService)
47
61
  : undefined;
@@ -51,11 +65,13 @@ export const createGlobalAuctionContext = (window, logger, eventService, config
51
65
  if (biddersDisablingConfig?.enabled ||
52
66
  previousBidCpmsConfig?.enabled ||
53
67
  frequencyCapConfig?.enabled ||
54
- interstitialConfig?.enabled) {
68
+ interstitialConfig?.enabled ||
69
+ anchorEnabled) {
55
70
  window.pbjs.que.push(() => {
56
71
  window.pbjs.onEvent('auctionEnd', auction => {
57
72
  biddersDisabling?.onAuctionEnd(auction);
58
73
  interstitial?.onAuctionEnd(auction);
74
+ anchor?.onAuctionEnd(auction);
59
75
  if (previousBidCpmsConfig?.enabled && auction.bidsReceived) {
60
76
  previousBidCpms?.onAuctionEnd(auction.bidsReceived);
61
77
  }
@@ -91,11 +107,12 @@ export const createGlobalAuctionContext = (window, logger, eventService, config
91
107
  frequencyCapping?.afterRequestAds();
92
108
  });
93
109
  }
94
- if (frequencyCapConfig?.enabled || interstitialConfig?.enabled) {
110
+ if (frequencyCapConfig?.enabled || interstitialConfig?.enabled || anchorEnabled) {
95
111
  window.googletag.cmd.push(() => {
96
112
  window.googletag.pubads().addEventListener('slotRenderEnded', event => {
97
113
  frequencyCapping?.onSlotRenderEnded(event);
98
114
  interstitial?.onSlotRenderEnded(event);
115
+ anchor?.onSlotRenderEnded(event);
99
116
  });
100
117
  window.googletag.pubads().addEventListener('impressionViewable', event => {
101
118
  frequencyCapping?.onImpressionViewable(event);
@@ -105,6 +122,7 @@ export const createGlobalAuctionContext = (window, logger, eventService, config
105
122
  const configureStep = mkConfigureStep('GlobalAuctionContext', context => {
106
123
  frequencyCapping?.updateAdUnitPaths(context.adUnitPathVariables__);
107
124
  interstitial?.updateAdUnitPaths(context.adUnitPathVariables__);
125
+ anchor?.updateAdUnitPaths(context.adUnitPathVariables__);
108
126
  rewardedAd?.updateAdUnitPaths(context.adUnitPathVariables__);
109
127
  return Promise.resolve();
110
128
  });
@@ -128,6 +146,12 @@ export const createGlobalAuctionContext = (window, logger, eventService, config
128
146
  interstitialChannel: () => {
129
147
  return interstitial?.interstitialChannel();
130
148
  },
149
+ anchorBottomChannel: (domId) => {
150
+ return anchor?.anchorBottomChannel(domId);
151
+ },
152
+ anchorTopChannel: () => {
153
+ return anchor?.anchorTopChannel();
154
+ },
131
155
  rewardedAd: () => {
132
156
  return rewardedAd?.requestRewardedAd() ?? Promise.resolve({ state: 'empty' });
133
157
  },
@@ -5,7 +5,7 @@ import { AssetLoadMethod } from '../util/assetLoaderService';
5
5
  import { tcfapi } from '../types/tcfapi';
6
6
  import { createTestSlots } from '../util/test-slots';
7
7
  import { resolveAdUnitPath } from './adUnitPath';
8
- import { formatKey, CUSTOM_INTERSTITIAL_FORMAT } from './keyValues';
8
+ import { formatKey, CUSTOM_INTERSTITIAL_FORMAT, CUSTOM_ANCHOR_BOTTOM_FORMAT, CUSTOM_ANCHOR_TOP_FORMAT } from './keyValues';
9
9
  const testAdSlot = (domId, adUnitPath) => ({
10
10
  setCollapseEmptyDiv() {
11
11
  return;
@@ -38,6 +38,11 @@ const testAdSlot = (domId, adUnitPath) => ({
38
38
  return;
39
39
  }
40
40
  });
41
+ const setCustomAnchorFormatTargeting = (adSlot, channel, customFormat) => {
42
+ if (channel === 'c') {
43
+ adSlot.setTargeting(formatKey, customFormat);
44
+ }
45
+ };
41
46
  const configureTargeting = (window, runtimeKeyValues, serverSideTargeting) => {
42
47
  const staticKeyValues = serverSideTargeting ? serverSideTargeting.keyValues : {};
43
48
  const excludes = serverSideTargeting?.adManagerExcludes ?? [];
@@ -231,6 +236,21 @@ export const gptDefineSlots = () => (context, slots) => {
231
236
  context.window__.document.body.appendChild(slot);
232
237
  }
233
238
  };
239
+ const defineAnchorSlot = (channel, outOfPageFormat) => {
240
+ switch (channel) {
241
+ case 'gam':
242
+ return [
243
+ context.window__.googletag.defineOutOfPageSlot(resolvedAdUnitPath, outOfPageFormat),
244
+ outOfPageFormat
245
+ ];
246
+ case 'c':
247
+ default:
248
+ return [
249
+ context.window__.googletag.defineSlot(resolvedAdUnitPath, sizes, moliSlot.domId),
250
+ null
251
+ ];
252
+ }
253
+ };
234
254
  const defineAdSlot = () => {
235
255
  switch (moliSlot.position) {
236
256
  case 'in-page':
@@ -276,6 +296,10 @@ export const gptDefineSlots = () => (context, slots) => {
276
296
  context.window__.googletag.defineOutOfPageSlot(resolvedAdUnitPath, context.window__.googletag.enums.OutOfPageFormat.TOP_ANCHOR),
277
297
  context.window__.googletag.enums.OutOfPageFormat.TOP_ANCHOR
278
298
  ];
299
+ case 'anchor-bottom':
300
+ return defineAnchorSlot(context.auction__.anchorBottomChannel(moliSlot.domId), context.window__.googletag.enums.OutOfPageFormat.BOTTOM_ANCHOR);
301
+ case 'anchor-top':
302
+ return defineAnchorSlot(context.auction__.anchorTopChannel(), context.window__.googletag.enums.OutOfPageFormat.TOP_ANCHOR);
279
303
  }
280
304
  };
281
305
  const defineAndDisplayAdSlot = () => {
@@ -292,6 +316,12 @@ export const gptDefineSlots = () => (context, slots) => {
292
316
  adSlot.setTargeting(formatKey, CUSTOM_INTERSTITIAL_FORMAT);
293
317
  }
294
318
  }
319
+ else if (moliSlot.position === 'anchor-bottom') {
320
+ setCustomAnchorFormatTargeting(adSlot, context.auction__.anchorBottomChannel(moliSlot.domId), CUSTOM_ANCHOR_BOTTOM_FORMAT);
321
+ }
322
+ else if (moliSlot.position === 'anchor-top') {
323
+ setCustomAnchorFormatTargeting(adSlot, context.auction__.anchorTopChannel(), CUSTOM_ANCHOR_TOP_FORMAT);
324
+ }
295
325
  else {
296
326
  adSlot.setConfig({ targeting: { [formatKey]: null } });
297
327
  }
@@ -1,2 +1,4 @@
1
1
  export const formatKey = 'f';
2
2
  export const CUSTOM_INTERSTITIAL_FORMAT = '100';
3
+ export const CUSTOM_ANCHOR_BOTTOM_FORMAT = '101';
4
+ export const CUSTOM_ANCHOR_TOP_FORMAT = '102';
package/lib/ads/prebid.js CHANGED
@@ -8,6 +8,7 @@ import { AssetLoadMethod } from '../util/assetLoaderService';
8
8
  import { packageJson } from 'ad-tag/gen/packageJson';
9
9
  import { prebidOutstreamRenderer } from 'ad-tag/ads/prebid-outstream';
10
10
  import { isGamInterstitial } from 'ad-tag/ads/auctions/interstitialContext';
11
+ import { isGamAnchor } from 'ad-tag/ads/auctions/anchorContext';
11
12
  import { criteoEnrichWithFpd } from 'ad-tag/ads/criteo';
12
13
  import { enrichId5WithFpd } from 'ad-tag/ads/id5';
13
14
  const prebidTimeout = (context) => {
@@ -257,13 +258,24 @@ export const prebidPrepareRequestAds = (prebidConfig) => mkPrepareRequestAdsStep
257
258
  resolve();
258
259
  }
259
260
  }));
261
+ const isGamAnchorChannel = ({ moliSlot }, context) => {
262
+ switch (moliSlot.position) {
263
+ case 'anchor-bottom':
264
+ return context.auction__.anchorBottomChannel(moliSlot.domId) === 'gam';
265
+ case 'anchor-top':
266
+ return context.auction__.anchorTopChannel() === 'gam';
267
+ default:
268
+ return false;
269
+ }
270
+ };
260
271
  export const prebidRequestBids = (prebidConfig, adServer) => mkRequestBidsStep('prebid-request-bids', (context, slots) => {
261
272
  const failsafeTimeout = Math.max((prebidConfig.config.bidderTimeout ?? 2000) + 3000, prebidConfig.failsafeTimeout ?? 0);
262
273
  const failsafe = new Promise(resolve => context.window__.setTimeout(resolve, failsafeTimeout));
263
274
  const auction = new Promise(resolve => {
264
275
  const slotsToRefresh = slots.filter(slot => !context.auction__.isSlotThrottled(slot.adSlot) &&
265
276
  (!isGamInterstitial(slot.adSlot, context.window__) ||
266
- context.auction__.interstitialChannel() !== 'gam'));
277
+ context.auction__.interstitialChannel() !== 'gam') &&
278
+ (!isGamAnchor(slot.adSlot, context.window__) || !isGamAnchorChannel(slot, context)));
267
279
  const requestObject = prebidConfig.ephemeralAdUnits
268
280
  ? {
269
281
  adUnits: createdAdUnits(context, prebidConfig, slotsToRefresh)
@@ -1,3 +1,3 @@
1
1
  export const packageJson = {
2
- version: '5.10.3'
2
+ version: '5.11.0'
3
3
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@highfivve/ad-tag",
3
- "version": "5.10.3",
3
+ "version": "5.11.0",
4
4
  "license": "Apache-2.0",
5
5
  "description": "An ad tag implementation called moli",
6
6
  "main": "./lib/index.js",