@highfivve/ad-tag 5.11.4 → 5.11.6

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.
@@ -1,9 +1,6 @@
1
1
  import { mkInitStep } from 'ad-tag/ads/adPipeline';
2
2
  import { isPlainObject } from 'ad-tag/util/objectUtils';
3
- const findAdSlot = (message, googletag) => googletag
4
- .pubads()
5
- .getSlots()
6
- .find(slot => slot.getSlotElementId() === message.domId || slot.getAdUnitPath() === message.adUnitPath);
3
+ import { findGoogletagSlot } from 'ad-tag/ads/findGoogletagSlot';
7
4
  const parseMessageData = (data) => {
8
5
  try {
9
6
  const message = typeof data === 'string' ? JSON.parse(data) : isPlainObject(data) ? data : null;
@@ -22,7 +19,7 @@ const parseMessageData = (data) => {
22
19
  };
23
20
  const handleRefresh = (message, context) => {
24
21
  const backfillMoliSlot = context.config__.slots.find(slot => slot.domId === message.domId && slot.behaviour.loaded === 'backfill');
25
- const adSlot = findAdSlot(message, context.window__.googletag);
22
+ const adSlot = findGoogletagSlot(message, context.window__.googletag);
26
23
  if (backfillMoliSlot && adSlot) {
27
24
  context.logger__.debug('bridge', `Refresh ad slot ${message.domId}`);
28
25
  context.window__.googletag.destroySlots([adSlot]);
@@ -32,7 +29,7 @@ const handleRefresh = (message, context) => {
32
29
  }
33
30
  };
34
31
  const handlePassback = (message, context) => {
35
- const adSlot = findAdSlot(message, context.window__.googletag);
32
+ const adSlot = findGoogletagSlot(message, context.window__.googletag);
36
33
  const passbackKey = 'passback';
37
34
  if (adSlot && adSlot.getTargeting(passbackKey).length === 0) {
38
35
  context.logger__.debug('passback', `Process passback for ad slot ${adSlot.getAdUnitPath()}`);
@@ -0,0 +1,9 @@
1
+ export const findGoogletagSlot = (reference, googletag) => googletag
2
+ .pubads()
3
+ .getSlots()
4
+ .find(slot => {
5
+ const slotDomId = slot.getSlotElementId();
6
+ const slotAdUnitPath = slot.getAdUnitPath();
7
+ return ((!!reference.domId && !!slotDomId && slotDomId === reference.domId) ||
8
+ (!!reference.adUnitPath && !!slotAdUnitPath && slotAdUnitPath === reference.adUnitPath));
9
+ });
@@ -1,4 +1,6 @@
1
1
  import { isAdvertiserIncluded } from 'ad-tag/ads/isAdvertiserIncluded';
2
+ import { formatKey } from 'ad-tag/ads/keyValues';
3
+ import { asViewabilityOverrideEntryList, resolveViewabilityOverride } from './viewabilityOverride';
2
4
  export class AdVisibilityService {
3
5
  constructor(userActivityService, refreshInterval, refreshIntervalOverrides, useIntersectionObserver, disableAdVisibilityChecks, viewabilityOverrides, window, logger) {
4
6
  this.userActivityService = userActivityService;
@@ -12,10 +14,10 @@ export class AdVisibilityService {
12
14
  this.isSlotTracked = (domId) => this.visibilityRecords.has(domId);
13
15
  this.removeSlotTracking = (slot) => {
14
16
  this.logger?.debug('AdVisibilityService', `removing slot visibility tracking for ${slot.getSlotElementId()}`, slot);
17
+ const record = this.visibilityRecords.get(slot.getSlotElementId());
15
18
  this.visibilityRecords.delete(slot.getSlotElementId());
16
- const observedSlot = this.observedDomElementForSlot(slot);
17
- if (this.intersectionObserver && observedSlot) {
18
- this.intersectionObserver.unobserve(observedSlot.target);
19
+ if (this.intersectionObserver && record) {
20
+ this.intersectionObserver.unobserve(record.target);
19
21
  }
20
22
  };
21
23
  this.handleGoogletagAdVisibilityChanged = (event) => {
@@ -45,7 +47,7 @@ export class AdVisibilityService {
45
47
  this.minimalAdVisibilityRatio = disableAdVisibilityChecks ? 0 : 0.5;
46
48
  this.visibilityRecords = new Map();
47
49
  const requiredIntersectionObserver = (useIntersectionObserver ||
48
- Object.values(viewabilityOverrides).some(entry => entry?.variant === 'css')) &&
50
+ Object.values(viewabilityOverrides).some(entryOrList => asViewabilityOverrideEntryList(entryOrList).some(entry => entry.variant === 'css'))) &&
49
51
  'IntersectionObserver' in this.window;
50
52
  if (requiredIntersectionObserver) {
51
53
  this.intersectionObserver = new this.window.IntersectionObserver(entries => this.handleObservedAdVisibilityChanged(entries), { threshold: this.minimalAdVisibilityRatio });
@@ -72,6 +74,7 @@ export class AdVisibilityService {
72
74
  const override = domElement.viewabilityOverride;
73
75
  this.visibilityRecords.set(slot.getSlotElementId(), {
74
76
  slot: slot,
77
+ target: domElement.target,
75
78
  latestStartVisible: this.disableAdVisibilityChecks ||
76
79
  (override?.variant === 'disabled' &&
77
80
  (override.disableAllAdVisibilityChecks ||
@@ -153,7 +156,8 @@ export class AdVisibilityService {
153
156
  }
154
157
  observedDomElementForSlot(slot) {
155
158
  const slotDomId = slot.getSlotElementId();
156
- const viewabilityOverride = this.viewabilityOverrides[slotDomId];
159
+ const [liveFormat] = slot.getTargeting(formatKey);
160
+ const viewabilityOverride = resolveViewabilityOverride(asViewabilityOverrideEntryList(this.viewabilityOverrides[slotDomId]), liveFormat);
157
161
  const adSlotElement = this.window.document.getElementById(slotDomId);
158
162
  const overrideElement = viewabilityOverride && viewabilityOverride.variant === 'css'
159
163
  ? this.window.document.querySelector(viewabilityOverride.cssSelector)
@@ -1,8 +1,10 @@
1
1
  import { AdVisibilityService } from './adVisibilityService';
2
2
  import { UserActivityService } from './userActivityService';
3
3
  import { mkConfigureStep } from '../../adPipeline';
4
- import { isNotNull } from 'ad-tag/util/arrayUtils';
5
4
  import { isAdvertiserIncluded } from 'ad-tag/ads/isAdvertiserIncluded';
5
+ import { formatKey } from 'ad-tag/ads/keyValues';
6
+ import { resolveAdUnitPath } from 'ad-tag/ads/adUnitPath';
7
+ import { asViewabilityOverrideEntryList, resolveViewabilityOverride } from './viewabilityOverride';
6
8
  export const createAdReload = () => {
7
9
  const name = 'moli-ad-reload';
8
10
  const defaultRefreshIntervalMs = 20000;
@@ -57,14 +59,29 @@ export const createAdReload = () => {
57
59
  };
58
60
  const reloadAdSlot = (config, ctx) => (googleTagSlot) => {
59
61
  const slotId = googleTagSlot.getSlotElementId();
60
- const moliSlot = ctx.config__.slots.find(moliSlot => moliSlot.domId === slotId);
62
+ const slotAdUnitPath = googleTagSlot.getAdUnitPath();
63
+ const moliSlot = ctx.config__.slots.find(candidate => {
64
+ if (candidate.domId === slotId) {
65
+ return true;
66
+ }
67
+ if (!slotAdUnitPath) {
68
+ return false;
69
+ }
70
+ try {
71
+ return (resolveAdUnitPath(candidate.adUnitPath, ctx.adUnitPathVariables__) === slotAdUnitPath);
72
+ }
73
+ catch (e) {
74
+ return false;
75
+ }
76
+ });
61
77
  if (moliSlot && moliSlot.behaviour.loaded !== 'infinite') {
62
78
  ctx.logger__.debug('AdReload', 'fired slot reload', moliSlot.domId);
63
79
  const sizesOverride = maybeOptimizeSlotForCls(config, moliSlot, googleTagSlot, ctx.logger__, ctx.window__);
64
80
  googleTagSlot.setTargeting(reloadKeyValue, 'true');
65
81
  const getBucketAndLoadingBehaviour = () => {
66
- const bucketOverride = config.viewabilityOverrides?.[slotId]?.refreshBucket;
67
- if (bucketOverride === true) {
82
+ const [liveFormat] = googleTagSlot.getTargeting(formatKey);
83
+ const matchedOverride = resolveViewabilityOverride(asViewabilityOverrideEntryList(config.viewabilityOverrides?.[moliSlot.domId]), liveFormat);
84
+ if (matchedOverride?.refreshBucket === true) {
68
85
  const loaded = moliSlot.behaviour.loaded;
69
86
  const bucket = moliSlot.behaviour.bucket;
70
87
  const bucketName = typeof bucket === 'string'
@@ -82,18 +99,21 @@ export const createAdReload = () => {
82
99
  }
83
100
  else {
84
101
  ctx.window__.moli
85
- .refreshAdSlot(slotId, {
102
+ .refreshAdSlot(moliSlot.domId, {
86
103
  loaded: moliSlot.behaviour.loaded,
87
104
  ...(sizesOverride && { sizesOverride: sizesOverride })
88
105
  })
89
- .catch(error => ctx.logger__.error('AdReload', `refreshing ${slotId} failed`, error));
106
+ .catch(error => ctx.logger__.error('AdReload', `refreshing ${moliSlot.domId} failed`, error));
90
107
  }
91
108
  }
92
109
  };
93
110
  const setupSlotRenderListener = (config, slotsToMonitor, reloadAdSlotCallback, window, logger) => window.googletag.pubads().addEventListener('slotRenderEnded', renderEndedEvent => {
94
111
  const { slot: googleTagSlot, campaignId, advertiserId, companyIds, yieldGroupIds, isEmpty: slotIsEmpty } = renderEndedEvent;
95
112
  const slotDomId = googleTagSlot.getSlotElementId();
96
- const slotIsMonitored = slotsToMonitor.indexOf(slotDomId) > -1;
113
+ const slotAdUnitPath = googleTagSlot.getAdUnitPath();
114
+ const monitoredSlot = slotsToMonitor.find(monitored => monitored.domId === slotDomId ||
115
+ (!!slotAdUnitPath && monitored.adUnitPath === slotAdUnitPath));
116
+ const slotIsMonitored = !!monitoredSlot;
97
117
  const orderIdNotExcluded = !campaignId || config.excludeOrderIds.indexOf(campaignId) === -1;
98
118
  const orderIdIncluded = !!campaignId && config.includeOrderIds.indexOf(campaignId) > -1;
99
119
  const advertiserIdIncluded = isAdvertiserIncluded(renderEndedEvent, config.includeAdvertiserIds);
@@ -114,7 +134,7 @@ export const createAdReload = () => {
114
134
  }
115
135
  const slotAlreadyTracked = !!adVisibilityService?.isSlotTracked(slotDomId);
116
136
  if (trackingSlotAllowed) {
117
- const bidderCode = globalAuctionContext?.getLastWinningBidderOfAdUnit(slotDomId);
137
+ const bidderCode = monitoredSlot && globalAuctionContext?.getLastWinningBidderOfAdUnit(monitoredSlot.domId);
118
138
  adVisibilityService.trackSlot(googleTagSlot, reloadAdSlotCallback, advertiserId, companyIds, bidderCode);
119
139
  }
120
140
  else if (slotAlreadyTracked) {
@@ -141,9 +161,19 @@ export const createAdReload = () => {
141
161
  ? [
142
162
  mkConfigureStep(name, context => {
143
163
  const slotsToMonitor = context.config__.slots
144
- .filter(slot => config.excludeAdSlotDomIds.indexOf(slot.domId) === -1)
145
- .map(slot => slot.domId)
146
- .filter(isNotNull);
164
+ .filter(slot => !!slot.domId && config.excludeAdSlotDomIds.indexOf(slot.domId) === -1)
165
+ .map(slot => {
166
+ try {
167
+ return {
168
+ domId: slot.domId,
169
+ adUnitPath: resolveAdUnitPath(slot.adUnitPath, context.adUnitPathVariables__)
170
+ };
171
+ }
172
+ catch (e) {
173
+ context.logger__.error('AdReload', `failed to resolve adUnitPath '${slot.adUnitPath}' for domId ${slot.domId}, monitoring by domId only`, e);
174
+ return { domId: slot.domId, adUnitPath: '' };
175
+ }
176
+ });
147
177
  const reloadAdSlotCallback = reloadAdSlot(config, context);
148
178
  context.logger__.debug('AdReload', 'monitoring slots', slotsToMonitor);
149
179
  initialize(context, config, slotsToMonitor, reloadAdSlotCallback);
@@ -0,0 +1,3 @@
1
+ export const asViewabilityOverrideEntryList = (entryOrList) => entryOrList === undefined ? [] : Array.isArray(entryOrList) ? entryOrList : [entryOrList];
2
+ export const isViewabilityOverrideConditionMatch = (conditions, liveFormat) => !conditions || conditions.format === undefined || conditions.format === liveFormat;
3
+ export const resolveViewabilityOverride = (entries, liveFormat) => entries.find(entry => isViewabilityOverrideConditionMatch(entry.conditions, liveFormat));
@@ -1,4 +1,6 @@
1
- import { mkPrepareRequestAdsStep, HIGH_PRIORITY, mkConfigureStepOncePerRequestAdsCycle } from 'ad-tag/ads/adPipeline';
1
+ import { mkPrepareRequestAdsStep, HIGH_PRIORITY, mkConfigureStep, mkConfigureStepOncePerRequestAdsCycle } from 'ad-tag/ads/adPipeline';
2
+ import { resolveAdUnitPath } from 'ad-tag/ads/adUnitPath';
3
+ import { findGoogletagSlot } from 'ad-tag/ads/findGoogletagSlot';
2
4
  export const createCleanup = () => {
3
5
  const name = 'cleanup';
4
6
  let cleanupConfig = null;
@@ -25,7 +27,7 @@ export const createCleanup = () => {
25
27
  });
26
28
  });
27
29
  }
28
- else {
30
+ else if ('jsAsString' in config.deleteMethod) {
29
31
  config.deleteMethod.jsAsString.forEach(jsLineAsString => {
30
32
  try {
31
33
  context.logger__.debug('Cleanup Module', `Try to execute string as JS: '${jsLineAsString}'`);
@@ -37,9 +39,30 @@ export const createCleanup = () => {
37
39
  }
38
40
  });
39
41
  }
42
+ else if ('destroySlot' in config.deleteMethod) {
43
+ let resolvedAdUnitPath;
44
+ try {
45
+ resolvedAdUnitPath = resolveAdUnitPath(config.deleteMethod.adUnitPath, context.adUnitPathVariables__);
46
+ }
47
+ catch (e) {
48
+ context.logger__.error('Cleanup Module', `failed to resolve adUnitPath '${config.deleteMethod.adUnitPath}' for domId ${config.domId}, skipping`, e);
49
+ return;
50
+ }
51
+ const googleTagSlot = findGoogletagSlot({ domId: config.domId, adUnitPath: resolvedAdUnitPath }, context.window__.googletag);
52
+ if (googleTagSlot) {
53
+ context.logger__.debug('Cleanup Module', `destroying stale gam slot for domId ${config.domId}`, googleTagSlot);
54
+ context.window__.googletag.destroySlots([googleTagSlot]);
55
+ }
56
+ else {
57
+ context.logger__.debug('Cleanup Module', `no gam slot found for domId ${config.domId} / adUnitPath ${resolvedAdUnitPath}, nothing to destroy`);
58
+ }
59
+ }
40
60
  });
41
61
  };
42
62
  const hasBidderWonLastAuction = (context, config) => {
63
+ if (!config.bidder) {
64
+ return true;
65
+ }
43
66
  const prebidWinningBids = context.window__.pbjs.getAllWinningBids();
44
67
  const bidderThatWonLastAuctionOnSlot = prebidWinningBids
45
68
  .filter(bid => bid.adUnitCode === config.domId)
@@ -55,10 +78,20 @@ export const createCleanup = () => {
55
78
  return Promise.resolve();
56
79
  }
57
80
  context.window__.pbjs.que.push(() => {
58
- const configsOfDomIdsThatNeedToBeCleaned = config.configs.filter(config => hasBidderWonLastAuction(context, config));
81
+ const configsOfDomIdsThatNeedToBeCleaned = config.configs.filter(config => !('destroySlot' in config.deleteMethod) &&
82
+ hasBidderWonLastAuction(context, config));
59
83
  cleanUp(context, configsOfDomIdsThatNeedToBeCleaned);
60
84
  });
61
85
  return Promise.resolve();
86
+ }),
87
+ mkConfigureStep('destroy-stale-gam-slot-before-redefine', (context, slots) => {
88
+ if (context.runtimeConfig__.environment === 'test') {
89
+ return Promise.resolve();
90
+ }
91
+ const domIdsThisCycle = slots.map(slot => slot.domId);
92
+ const configsToDestroy = config.configs.filter(config => 'destroySlot' in config.deleteMethod && domIdsThisCycle.includes(config.domId));
93
+ cleanUp(context, configsToDestroy);
94
+ return Promise.resolve();
62
95
  })
63
96
  ]
64
97
  : [];
@@ -74,6 +107,7 @@ export const createCleanup = () => {
74
107
  context.window__.pbjs.que.push(() => {
75
108
  const configsOfDomIdsThatNeedToBeCleaned = config.configs
76
109
  .filter(config => slots.map(slot => slot.moliSlot.domId).includes(config.domId))
110
+ .filter(config => !('destroySlot' in config.deleteMethod))
77
111
  .filter(config => hasBidderWonLastAuction(context, config));
78
112
  cleanUp(context, configsOfDomIdsThatNeedToBeCleaned);
79
113
  });
@@ -1,6 +1,7 @@
1
1
  import React, { Fragment } from 'react';
2
2
  import { Tag, TagLabel } from './tag';
3
3
  import { SubHeadline, TagContainer } from './ui';
4
+ import { asViewabilityOverrideEntryList } from 'ad-tag/ads/modules/ad-reload/viewabilityOverride';
4
5
  const BoolTag = ({ value, trueLabel, falseLabel }) => (React.createElement(Tag, { variant: value ? 'green' : 'grey' }, value ? (trueLabel ?? 'yes') : (falseLabel ?? 'no')));
5
6
  const Row = ({ label, subEntry, children }) => (React.createElement(TagContainer, { subEntry: subEntry },
6
7
  React.createElement(TagLabel, null, label),
@@ -70,12 +71,15 @@ const AdReloadModule = ({ config }) => (React.createElement(React.Fragment, null
70
71
  "ms")))))))))),
71
72
  config.viewabilityOverrides && Object.keys(config.viewabilityOverrides).length > 0 && (React.createElement(React.Fragment, null,
72
73
  React.createElement(SubHeadline, null, "Viewability overrides"),
73
- Object.entries(config.viewabilityOverrides).map(([domId, override]) => (React.createElement(Row, { key: domId, label: domId, subEntry: true },
74
- override?.variant === 'css' && React.createElement(Tag, { variant: "grey" },
74
+ Object.entries(config.viewabilityOverrides).flatMap(([domId, entryOrList]) => asViewabilityOverrideEntryList(entryOrList).map((override, index) => (React.createElement(Row, { key: `${domId}-${index}`, label: index === 0 ? domId : '', subEntry: true },
75
+ override.conditions?.format && (React.createElement(Tag, { variant: "blue" },
76
+ "format: ",
77
+ override.conditions.format)),
78
+ override.variant === 'css' && React.createElement(Tag, { variant: "grey" },
75
79
  "css: ",
76
80
  override.cssSelector),
77
- override?.variant === 'disabled' && React.createElement(Tag, { variant: "yellow" }, "checks disabled"),
78
- override?.refreshBucket && React.createElement(Tag, { variant: "yellow" }, "refreshes bucket"))))))));
81
+ override.variant === 'disabled' && React.createElement(Tag, { variant: "yellow" }, "checks disabled"),
82
+ override.refreshBucket && React.createElement(Tag, { variant: "yellow" }, "refreshes bucket")))))))));
79
83
  const BlocklistModule = ({ config }) => (React.createElement(React.Fragment, null,
80
84
  React.createElement(Row, { label: "Mode" },
81
85
  React.createElement(Tag, { variant: config.mode === 'block' ? 'red' : 'yellow' }, config.mode)),
@@ -95,13 +99,15 @@ const BlocklistModule = ({ config }) => (React.createElement(React.Fragment, nul
95
99
  const CleanupModule = ({ config }) => (React.createElement(React.Fragment, null,
96
100
  config.configs.length === 0 && React.createElement("i", null, "No cleanup configs"),
97
101
  config.configs.map((cleanupConfig, index) => (React.createElement(Row, { key: index, label: cleanupConfig.domId },
98
- React.createElement(Tag, { variant: "blue" }, cleanupConfig.bidder),
102
+ cleanupConfig.bidder && React.createElement(Tag, { variant: "blue" }, cleanupConfig.bidder),
99
103
  'cssSelectors' in cleanupConfig.deleteMethod ? (React.createElement(Tag, { variant: "grey" },
100
104
  "css: ",
101
- cleanupConfig.deleteMethod.cssSelectors.join(', '))) : (React.createElement(Tag, { variant: "yellow" },
105
+ cleanupConfig.deleteMethod.cssSelectors.join(', '))) : 'jsAsString' in cleanupConfig.deleteMethod ? (React.createElement(Tag, { variant: "yellow" },
102
106
  "JS snippet (",
103
107
  cleanupConfig.deleteMethod.jsAsString.length,
104
- ")")))))));
108
+ ")")) : (React.createElement(Tag, { variant: "yellow" },
109
+ "destroy GAM slot: ",
110
+ cleanupConfig.deleteMethod.adUnitPath)))))));
105
111
  const CustomModule = ({ config }) => (React.createElement(React.Fragment, null,
106
112
  React.createElement(Row, { label: "Inline JS" }, config.inlineJs ? (React.createElement(Tag, { variant: "yellow" },
107
113
  config.inlineJs.code.length,
@@ -1,3 +1,3 @@
1
1
  export const packageJson = {
2
- version: '5.11.4'
2
+ version: '5.11.6'
3
3
  };
@@ -60,16 +60,16 @@ function getSourceLabelStyle(source) {
60
60
  export function getDefaultLogger() {
61
61
  return {
62
62
  debug(source, message, ...optionalParams) {
63
- console.debug(`%c[DEBUG]%c${source}%c${message}`, getLogStageLabelStyle('debug'), getSourceLabelStyle(source), '', ...optionalParams);
63
+ console.debug(`%c[DEBUG] %c${source} %c${message}`, getLogStageLabelStyle('debug'), getSourceLabelStyle(source), '', ...optionalParams);
64
64
  },
65
65
  info(source, message, ...optionalParams) {
66
- console.info(`%c[INFO]%c${source}%c${message}`, getLogStageLabelStyle('info'), getSourceLabelStyle(source), '', ...optionalParams);
66
+ console.info(`%c[INFO] %c${source} %c${message}`, getLogStageLabelStyle('info'), getSourceLabelStyle(source), '', ...optionalParams);
67
67
  },
68
68
  warn(source, message, ...optionalParams) {
69
- console.warn(`%c[WARN]%c${source}%c${message}`, getLogStageLabelStyle('warn'), getSourceLabelStyle(source), '', ...optionalParams);
69
+ console.warn(`%c[WARN] %c${source} %c${message}`, getLogStageLabelStyle('warn'), getSourceLabelStyle(source), '', ...optionalParams);
70
70
  },
71
71
  error(source, message, ...optionalParams) {
72
- console.error(`%c[ERROR]%c${source}%c${message}`, getLogStageLabelStyle('error'), getSourceLabelStyle(source), '', ...optionalParams);
72
+ console.error(`%c[ERROR] %c${source} %c${message}`, getLogStageLabelStyle('error'), getSourceLabelStyle(source), '', ...optionalParams);
73
73
  }
74
74
  };
75
75
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@highfivve/ad-tag",
3
- "version": "5.11.4",
3
+ "version": "5.11.6",
4
4
  "license": "Apache-2.0",
5
5
  "description": "An ad tag implementation called moli",
6
6
  "main": "./lib/index.js",