@highfivve/ad-tag 5.5.4
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.
- package/LICENSE +201 -0
- package/README.md +149 -0
- package/lib/ads/a9.js +190 -0
- package/lib/ads/adPipeline.js +159 -0
- package/lib/ads/adService.js +251 -0
- package/lib/ads/adUnitPath.js +37 -0
- package/lib/ads/auctions/adRequestThrottling.js +22 -0
- package/lib/ads/auctions/biddersDisabling.js +90 -0
- package/lib/ads/auctions/frequencyCapping.js +189 -0
- package/lib/ads/auctions/interstitialContext.js +92 -0
- package/lib/ads/auctions/previousBidCpms.js +33 -0
- package/lib/ads/auctions/resume.js +3 -0
- package/lib/ads/bridge/bridge.js +62 -0
- package/lib/ads/consent.js +63 -0
- package/lib/ads/eventService.js +44 -0
- package/lib/ads/globalAuctionContext.js +98 -0
- package/lib/ads/googleAdManager.js +380 -0
- package/lib/ads/keyValues.js +1 -0
- package/lib/ads/labelConfigService.js +42 -0
- package/lib/ads/modules/ad-reload/adVisibilityService.js +158 -0
- package/lib/ads/modules/ad-reload/index.js +163 -0
- package/lib/ads/modules/ad-reload/userActivityService.js +70 -0
- package/lib/ads/modules/adex/adex-mapping.js +77 -0
- package/lib/ads/modules/adex/adexUtiq.js +15 -0
- package/lib/ads/modules/adex/index.js +142 -0
- package/lib/ads/modules/adex/sendAdvertisingId.js +20 -0
- package/lib/ads/modules/blocklist-url/index.js +118 -0
- package/lib/ads/modules/cleanup/index.js +93 -0
- package/lib/ads/modules/confiant/index.js +47 -0
- package/lib/ads/modules/emetriq/index.js +154 -0
- package/lib/ads/modules/emetriq/trackInApp.js +34 -0
- package/lib/ads/modules/emetriq/trackLoginEvent.js +59 -0
- package/lib/ads/modules/generic-skin/index.js +150 -0
- package/lib/ads/modules/identitylink/index.js +58 -0
- package/lib/ads/modules/interstitial/index.js +38 -0
- package/lib/ads/modules/interstitial/interstitialAd.js +111 -0
- package/lib/ads/modules/lazy-load/index.js +191 -0
- package/lib/ads/modules/lazy-load/selectInfiniteSlot.js +11 -0
- package/lib/ads/modules/prebid-first-party-data/index.js +115 -0
- package/lib/ads/modules/pubstack/index.js +59 -0
- package/lib/ads/modules/sticky-footer-ad/desktopFloorAd.js +63 -0
- package/lib/ads/modules/sticky-footer-ad/index.js +43 -0
- package/lib/ads/modules/sticky-footer-ad/mobileSticky.js +93 -0
- package/lib/ads/modules/sticky-footer-ad-v2/footerStickyAd.js +118 -0
- package/lib/ads/modules/sticky-footer-ad-v2/index.js +43 -0
- package/lib/ads/modules/sticky-header-ad/fadeOutCallback.js +25 -0
- package/lib/ads/modules/sticky-header-ad/index.js +103 -0
- package/lib/ads/modules/sticky-header-ad/renderResult.js +24 -0
- package/lib/ads/modules/utiq/index.js +79 -0
- package/lib/ads/modules/yield-optimization/dynamicFloorPrice.js +86 -0
- package/lib/ads/modules/yield-optimization/index.js +57 -0
- package/lib/ads/modules/yield-optimization/isYieldOptimizationConfigDynamic.js +6 -0
- package/lib/ads/modules/yield-optimization/yieldOptimizationService.js +169 -0
- package/lib/ads/modules/zeotap/index.js +111 -0
- package/lib/ads/moli.js +645 -0
- package/lib/ads/moliGlobal.js +11 -0
- package/lib/ads/prebid-outstream.js +13 -0
- package/lib/ads/prebid.js +406 -0
- package/lib/ads/sizeConfigService.js +49 -0
- package/lib/ads/spa.js +32 -0
- package/lib/bundle/adReload.js +2 -0
- package/lib/bundle/adex.js +2 -0
- package/lib/bundle/blocklistUrls.js +2 -0
- package/lib/bundle/cleanup.js +2 -0
- package/lib/bundle/confiant.js +2 -0
- package/lib/bundle/configureFromEndpoint.js +40 -0
- package/lib/bundle/emetriq.js +2 -0
- package/lib/bundle/identityLink.js +2 -0
- package/lib/bundle/init.js +2 -0
- package/lib/bundle/interstitialModule.js +2 -0
- package/lib/bundle/lazyLoad.js +2 -0
- package/lib/bundle/prebidFirstPartyData.js +2 -0
- package/lib/bundle/pubstack.js +2 -0
- package/lib/bundle/skin.js +2 -0
- package/lib/bundle/stickyFooterAd.js +2 -0
- package/lib/bundle/stickyFooterAds2.js +2 -0
- package/lib/bundle/stickyHeaderAd.js +2 -0
- package/lib/bundle/utiq.js +2 -0
- package/lib/bundle/yieldOptimization.js +2 -0
- package/lib/bundle/zeotap.js +2 -0
- package/lib/console/components/adSlotConfig.js +225 -0
- package/lib/console/components/consentConfig.js +138 -0
- package/lib/console/components/globalConfig.js +634 -0
- package/lib/console/components/labelConfigDebug.js +19 -0
- package/lib/console/components/sizeConfigDebug.js +34 -0
- package/lib/console/components/tag.js +4 -0
- package/lib/console/debug.js +38 -0
- package/lib/console/util/array.js +1 -0
- package/lib/console/util/calculateAdDensity.js +36 -0
- package/lib/console/util/debounce.js +12 -0
- package/lib/console/util/debugLogger.js +6 -0
- package/lib/console/util/extractPositionFromPath.js +8 -0
- package/lib/console/util/prebid.js +3 -0
- package/lib/console/util/stringUtils.js +23 -0
- package/lib/console/util/themingService.js +31 -0
- package/lib/console/util/windowResizeService.js +22 -0
- package/lib/console/validations/adReloadValidations.js +47 -0
- package/lib/console/validations/bucketValidations.js +78 -0
- package/lib/console/validations/sizesConfigValidations.js +97 -0
- package/lib/gen/packageJson.js +3 -0
- package/lib/index.js +3 -0
- package/lib/types/apstag.js +1 -0
- package/lib/types/dom.js +1 -0
- package/lib/types/emetriq.js +1 -0
- package/lib/types/googletag.js +1 -0
- package/lib/types/identitylink.js +1 -0
- package/lib/types/module.js +1 -0
- package/lib/types/moliConfig.js +1 -0
- package/lib/types/moliRuntime.js +1 -0
- package/lib/types/prebidjs.js +45 -0
- package/lib/types/supplyChainObject.js +1 -0
- package/lib/types/tcfapi.js +48 -0
- package/lib/util/addNewInfiniteSlotToConfig.js +11 -0
- package/lib/util/arrayUtils.js +9 -0
- package/lib/util/assetLoaderService.js +91 -0
- package/lib/util/browserStorageKeys.js +7 -0
- package/lib/util/debugDelay.js +12 -0
- package/lib/util/domready.js +15 -0
- package/lib/util/environmentOverride.js +16 -0
- package/lib/util/extractAdTagVersion.js +16 -0
- package/lib/util/extractTopPrivateDomainFromHostname.js +17 -0
- package/lib/util/localStorage.js +26 -0
- package/lib/util/logging.js +106 -0
- package/lib/util/objectUtils.js +29 -0
- package/lib/util/query.js +40 -0
- package/lib/util/queryParameters.js +5 -0
- package/lib/util/resolveOverrides.js +21 -0
- package/lib/util/resolveStoredRequestIdInOrtb2Object.js +12 -0
- package/lib/util/sizes.js +3 -0
- package/lib/util/test-slots.js +150 -0
- package/lib/util/uuid.js +10 -0
- package/package.json +127 -0
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { AdVisibilityService } from './adVisibilityService';
|
|
2
|
+
import { UserActivityService } from './userActivityService';
|
|
3
|
+
import { mkConfigureStep } from '../../adPipeline';
|
|
4
|
+
import { isNotNull } from 'ad-tag/util/arrayUtils';
|
|
5
|
+
export class AdReload {
|
|
6
|
+
constructor() {
|
|
7
|
+
this.name = 'moli-ad-reload';
|
|
8
|
+
this.description = 'Moli implementation of an ad reload module.';
|
|
9
|
+
this.moduleType = 'ad-reload';
|
|
10
|
+
this.moduleConfig = null;
|
|
11
|
+
this.refreshIntervalMs = 20000;
|
|
12
|
+
this.reloadKeyValue = 'native-ad-reload';
|
|
13
|
+
this.initialized = false;
|
|
14
|
+
this.initialize = (context, config, slotsToMonitor, reloadAdSlotCallback) => {
|
|
15
|
+
if (context.env__ === 'test') {
|
|
16
|
+
context.logger__.info('AdReload', 'disabled in environment test');
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
if (this.initialized) {
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
context.logger__.debug('AdReload', 'initialize moli ad reload module');
|
|
23
|
+
this.setupAdVisibilityService(config, context.window__, context.logger__);
|
|
24
|
+
this.setupSlotRenderListener(config, slotsToMonitor, reloadAdSlotCallback, context.window__, context.logger__);
|
|
25
|
+
this.initialized = true;
|
|
26
|
+
};
|
|
27
|
+
this.setupAdVisibilityService = (config, window, logger) => {
|
|
28
|
+
this.adVisibilityService = new AdVisibilityService(new UserActivityService(window, config.userActivityLevelControl, logger), this.refreshIntervalMs, config.refreshIntervalMsOverrides ?? {}, false, !!config.disableAdVisibilityChecks, config.viewabilityOverrides ?? {}, window, logger);
|
|
29
|
+
};
|
|
30
|
+
this.setupSlotRenderListener = (config, slotsToMonitor, reloadAdSlotCallback, window, logger) => window.googletag.pubads().addEventListener('slotRenderEnded', renderEndedEvent => {
|
|
31
|
+
const { slot: googleTagSlot, campaignId, advertiserId, yieldGroupIds, isEmpty: slotIsEmpty } = renderEndedEvent;
|
|
32
|
+
const slotDomId = googleTagSlot.getSlotElementId();
|
|
33
|
+
const slotIsMonitored = slotsToMonitor.indexOf(slotDomId) > -1;
|
|
34
|
+
const orderIdNotExcluded = !campaignId || config.excludeOrderIds.indexOf(campaignId) === -1;
|
|
35
|
+
const orderIdIncluded = !!campaignId && config.includeOrderIds.indexOf(campaignId) > -1;
|
|
36
|
+
const advertiserIdIncluded = !!advertiserId && config.includeAdvertiserIds.indexOf(advertiserId) > -1;
|
|
37
|
+
const yieldGroupIdIncluded = !!yieldGroupIds && config.includeYieldGroupIds.some(id => yieldGroupIds.indexOf(id) > -1);
|
|
38
|
+
const trackingSlotAllowed = !slotIsEmpty &&
|
|
39
|
+
slotIsMonitored &&
|
|
40
|
+
orderIdNotExcluded &&
|
|
41
|
+
(orderIdIncluded || advertiserIdIncluded || yieldGroupIdIncluded);
|
|
42
|
+
if (!trackingSlotAllowed) {
|
|
43
|
+
this.logTrackingDisallowedReason(slotDomId, {
|
|
44
|
+
slotIsEmpty,
|
|
45
|
+
slotIsMonitored,
|
|
46
|
+
orderIdNotExcluded,
|
|
47
|
+
orderIdIncluded,
|
|
48
|
+
advertiserIdIncluded,
|
|
49
|
+
yieldGroupIdIncluded
|
|
50
|
+
}, logger, yieldGroupIds, campaignId, advertiserId);
|
|
51
|
+
}
|
|
52
|
+
const slotAlreadyTracked = !!this.adVisibilityService?.isSlotTracked(slotDomId);
|
|
53
|
+
if (trackingSlotAllowed) {
|
|
54
|
+
this.adVisibilityService.trackSlot(googleTagSlot, reloadAdSlotCallback, advertiserId);
|
|
55
|
+
}
|
|
56
|
+
else if (slotAlreadyTracked) {
|
|
57
|
+
this.adVisibilityService.removeSlotTracking(googleTagSlot);
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
this.reloadAdSlot = (config, ctx) => (googleTagSlot) => {
|
|
61
|
+
const slotId = googleTagSlot.getSlotElementId();
|
|
62
|
+
const moliSlot = ctx.config__.slots.find(moliSlot => moliSlot.domId === slotId);
|
|
63
|
+
if (moliSlot && moliSlot.behaviour.loaded !== 'infinite') {
|
|
64
|
+
ctx.logger__.debug('AdReload', 'fired slot reload', moliSlot.domId);
|
|
65
|
+
const sizesOverride = this.maybeOptimizeSlotForCls(config, moliSlot, googleTagSlot, ctx.logger__, ctx.window__);
|
|
66
|
+
googleTagSlot.setTargeting(this.reloadKeyValue, 'true');
|
|
67
|
+
const getBucketAndLoadingBehaviour = () => {
|
|
68
|
+
const bucketOverride = config.viewabilityOverrides?.[slotId]?.refreshBucket;
|
|
69
|
+
if (bucketOverride === true) {
|
|
70
|
+
const loaded = moliSlot.behaviour.loaded;
|
|
71
|
+
const bucket = moliSlot.behaviour.bucket;
|
|
72
|
+
const bucketName = typeof bucket === 'string'
|
|
73
|
+
? bucket
|
|
74
|
+
: bucket?.[ctx.labelConfigService__.getDeviceLabel()];
|
|
75
|
+
return bucketName && loaded !== 'infinite' ? { name: bucketName, loaded } : undefined;
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
const bucket = getBucketAndLoadingBehaviour();
|
|
79
|
+
if (bucket) {
|
|
80
|
+
ctx.window__.moli
|
|
81
|
+
.refreshBucket(bucket.name, { loaded: bucket.loaded })
|
|
82
|
+
.then(result => ctx.logger__.debug('AdReload', `refreshBucket '${bucket.name}' result`, result))
|
|
83
|
+
.catch(error => ctx.logger__.error('AdReload', `refreshBucket '${bucket.name}' failed`, error));
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
ctx.window__.moli
|
|
87
|
+
.refreshAdSlot(slotId, {
|
|
88
|
+
loaded: moliSlot.behaviour.loaded,
|
|
89
|
+
...(sizesOverride && { sizesOverride: sizesOverride })
|
|
90
|
+
})
|
|
91
|
+
.catch(error => ctx.logger__.error('AdReload', `refreshing ${slotId} failed`, error));
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
this.maybeOptimizeSlotForCls = (config, moliSlot, googleTagSlot, logger, _window) => {
|
|
96
|
+
const slotDomId = moliSlot.domId;
|
|
97
|
+
if (config.optimizeClsScoreDomIds.indexOf(slotDomId) > -1) {
|
|
98
|
+
const slotDomElement = _window.document.getElementById(slotDomId);
|
|
99
|
+
if (slotDomElement && !!slotDomElement.style) {
|
|
100
|
+
const slotHeight = slotDomElement.scrollHeight;
|
|
101
|
+
slotDomElement.style.setProperty('height', `${slotHeight}px`);
|
|
102
|
+
const newSlotSizes = moliSlot.sizes.filter(size => size !== 'fluid' && size[1] <= slotHeight);
|
|
103
|
+
logger.debug('AdReload', `CLS optimization: slot ${slotDomId} received fixed height ${slotHeight}px`, 'new sizes:', newSlotSizes);
|
|
104
|
+
if (newSlotSizes.length < moliSlot.sizes.filter(size => size !== 'fluid').length) {
|
|
105
|
+
_window.googletag.destroySlots([googleTagSlot]);
|
|
106
|
+
}
|
|
107
|
+
return newSlotSizes;
|
|
108
|
+
}
|
|
109
|
+
logger.warn('AdReload', `CLS optimization: slot ${slotDomId} to be optimized but not found in DOM.`);
|
|
110
|
+
}
|
|
111
|
+
return moliSlot.sizes;
|
|
112
|
+
};
|
|
113
|
+
this.logTrackingDisallowedReason = (slotDomId, reasons, logger, yieldGroupIds, campaignId, advertiserId) => {
|
|
114
|
+
const { slotIsEmpty, slotIsMonitored, orderIdNotExcluded, orderIdIncluded, advertiserIdIncluded, yieldGroupIdIncluded } = reasons;
|
|
115
|
+
if (slotIsEmpty) {
|
|
116
|
+
logger.debug('AdReload', slotDomId, 'slot not tracked: reported empty');
|
|
117
|
+
}
|
|
118
|
+
if (!slotIsMonitored) {
|
|
119
|
+
logger.debug('AdReload', slotDomId, 'slot not tracked: excluded by DOM id');
|
|
120
|
+
}
|
|
121
|
+
if (!orderIdNotExcluded) {
|
|
122
|
+
logger.debug('AdReload', slotDomId, 'slot not tracked: excluded by order id', campaignId);
|
|
123
|
+
}
|
|
124
|
+
if (!(orderIdIncluded || advertiserIdIncluded || yieldGroupIdIncluded)) {
|
|
125
|
+
logger.debug('AdReload', slotDomId, 'slot not tracked: neither order id', campaignId, 'nor advertiser id', advertiserId, 'nor yieldGroup id', yieldGroupIds, 'included');
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
config__() {
|
|
130
|
+
return this.moduleConfig;
|
|
131
|
+
}
|
|
132
|
+
isInitialized() {
|
|
133
|
+
return this.initialized;
|
|
134
|
+
}
|
|
135
|
+
configure__(moduleConfig) {
|
|
136
|
+
if (moduleConfig?.adReload?.enabled) {
|
|
137
|
+
this.moduleConfig = moduleConfig.adReload;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
initSteps__() {
|
|
141
|
+
return [];
|
|
142
|
+
}
|
|
143
|
+
configureSteps__() {
|
|
144
|
+
const config = this.moduleConfig;
|
|
145
|
+
return config
|
|
146
|
+
? [
|
|
147
|
+
mkConfigureStep(this.name, context => {
|
|
148
|
+
const slotsToMonitor = context.config__.slots
|
|
149
|
+
.filter(slot => config.excludeAdSlotDomIds.indexOf(slot.domId) === -1)
|
|
150
|
+
.map(slot => slot.domId)
|
|
151
|
+
.filter(isNotNull);
|
|
152
|
+
const reloadAdSlotCallback = this.reloadAdSlot(config, context);
|
|
153
|
+
context.logger__.debug('AdReload', 'monitoring slots', slotsToMonitor);
|
|
154
|
+
this.initialize(context, config, slotsToMonitor, reloadAdSlotCallback);
|
|
155
|
+
return Promise.resolve();
|
|
156
|
+
})
|
|
157
|
+
]
|
|
158
|
+
: [];
|
|
159
|
+
}
|
|
160
|
+
prepareRequestAdsSteps__() {
|
|
161
|
+
return [];
|
|
162
|
+
}
|
|
163
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
export const userActivityParametersForLevel = new Map([
|
|
2
|
+
['strict', { userActivityDuration: 10 * 1000, userBecomingInactiveDuration: 5 * 1000 }],
|
|
3
|
+
['moderate', { userActivityDuration: 12 * 1000, userBecomingInactiveDuration: 8 * 1000 }],
|
|
4
|
+
['lax', { userActivityDuration: 15 * 1000, userBecomingInactiveDuration: 12 * 1000 }]
|
|
5
|
+
]);
|
|
6
|
+
export class UserActivityService {
|
|
7
|
+
constructor(window, userActivityLevelControl = {
|
|
8
|
+
level: 'strict'
|
|
9
|
+
}, logger) {
|
|
10
|
+
this.window = window;
|
|
11
|
+
this.userActivityLevelControl = userActivityLevelControl;
|
|
12
|
+
this.logger = logger;
|
|
13
|
+
this.isActive = true;
|
|
14
|
+
this.handleUserInteraction = () => this.userActive();
|
|
15
|
+
this.handleUserIdle = () => this.userInactive();
|
|
16
|
+
this.handleUserBecomingIdle = () => this.userBecomingInactive();
|
|
17
|
+
this.handlePageVisibilityChanged = () => {
|
|
18
|
+
if (this.window.document.hidden) {
|
|
19
|
+
this.userInactive();
|
|
20
|
+
}
|
|
21
|
+
else {
|
|
22
|
+
this.userActive();
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
this.listener = [];
|
|
26
|
+
this.window.document.addEventListener('visibilitychange', this.handlePageVisibilityChanged);
|
|
27
|
+
this.logger?.debug('UserActivityService', 'initialized');
|
|
28
|
+
switch (userActivityLevelControl.level) {
|
|
29
|
+
case 'custom':
|
|
30
|
+
this.userActivityDuration = userActivityLevelControl.userActivityDuration;
|
|
31
|
+
this.userBecomingInactiveDuration = userActivityLevelControl.userBecomingInactiveDuration;
|
|
32
|
+
break;
|
|
33
|
+
default:
|
|
34
|
+
const { userActivityDuration, userBecomingInactiveDuration } = userActivityParametersForLevel.get(userActivityLevelControl.level);
|
|
35
|
+
this.userActivityDuration = userActivityDuration;
|
|
36
|
+
this.userBecomingInactiveDuration = userBecomingInactiveDuration;
|
|
37
|
+
}
|
|
38
|
+
this.userActive();
|
|
39
|
+
}
|
|
40
|
+
addUserActivityChangedListener(listener) {
|
|
41
|
+
this.listener.push(listener);
|
|
42
|
+
}
|
|
43
|
+
userActive() {
|
|
44
|
+
if (!this.isActive) {
|
|
45
|
+
this.isActive = true;
|
|
46
|
+
this.listener.forEach(listener => listener(this.isActive));
|
|
47
|
+
this.logger?.debug('UserActivityService', 'user has become active');
|
|
48
|
+
}
|
|
49
|
+
UserActivityService.observedEvents.forEach(event => this.window.removeEventListener(event, this.handleUserInteraction));
|
|
50
|
+
if (this.userInactiveTimer) {
|
|
51
|
+
this.window.clearTimeout(this.userInactiveTimer);
|
|
52
|
+
}
|
|
53
|
+
if (this.userBecomingIdleTimer) {
|
|
54
|
+
this.window.clearTimeout(this.userBecomingIdleTimer);
|
|
55
|
+
}
|
|
56
|
+
this.userInactiveTimer = this.window.setTimeout(this.handleUserIdle, this.userActivityDuration);
|
|
57
|
+
this.userBecomingIdleTimer = this.window.setTimeout(this.handleUserBecomingIdle, this.userBecomingInactiveDuration);
|
|
58
|
+
}
|
|
59
|
+
userInactive() {
|
|
60
|
+
this.isActive = false;
|
|
61
|
+
UserActivityService.observedEvents.forEach(event => this.window.addEventListener(event, this.handleUserInteraction));
|
|
62
|
+
this.listener.forEach(listener => listener(this.isActive));
|
|
63
|
+
this.logger?.debug('UserActivityService', 'user has become inactive');
|
|
64
|
+
}
|
|
65
|
+
userBecomingInactive() {
|
|
66
|
+
UserActivityService.observedEvents.forEach(event => this.window.addEventListener(event, this.handleUserInteraction));
|
|
67
|
+
this.logger?.debug('UserActivityService', 'user will shortly become inactive');
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
UserActivityService.observedEvents = ['mousemove', 'touchstart', 'scroll', 'keypress'];
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
export const toAdexMapType = (keyValueMap, mappingDefinition, logger) => {
|
|
2
|
+
const mapKey = keyValueMap[mappingDefinition.key];
|
|
3
|
+
const mapValue = extractStringOrNumber(keyValueMap, mappingDefinition.valueType, mappingDefinition.valueKey);
|
|
4
|
+
if (!mapKey ||
|
|
5
|
+
Array.isArray(mapKey) ||
|
|
6
|
+
(mapValue === undefined && mappingDefinition.defaultValue === undefined) ||
|
|
7
|
+
(mappingDefinition.valueType === 'number' &&
|
|
8
|
+
Number.isNaN(mapValue) &&
|
|
9
|
+
mappingDefinition.defaultValue === undefined)) {
|
|
10
|
+
logger.warn('Adex DMP', `values for key ${mappingDefinition.key}/valueKey ${mappingDefinition.valueKey} were empty or key was an array. Key/Value:`, mapKey, mapValue);
|
|
11
|
+
return undefined;
|
|
12
|
+
}
|
|
13
|
+
const adexTargetValue = convertToAdexTargetValue(mapValue, mappingDefinition, logger);
|
|
14
|
+
return {
|
|
15
|
+
[mappingDefinition.attribute]: {
|
|
16
|
+
[mapKey]: adexTargetValue
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
};
|
|
20
|
+
export const toAdexStringOrNumberType = (keyValueMap, mappingDefinition, logger) => {
|
|
21
|
+
const value = extractStringOrNumber(keyValueMap, mappingDefinition.adexValueType, mappingDefinition.key);
|
|
22
|
+
if ((value === undefined && mappingDefinition.defaultValue === undefined) ||
|
|
23
|
+
(mappingDefinition.adexValueType === 'number' &&
|
|
24
|
+
((Number.isNaN(value) && mappingDefinition.defaultValue === undefined) ||
|
|
25
|
+
Array.isArray(value)))) {
|
|
26
|
+
logger.warn('Adex DMP', `value for key ${mappingDefinition.key} was empty or key was an array. Value:`, value);
|
|
27
|
+
return undefined;
|
|
28
|
+
}
|
|
29
|
+
const adexTargetValue = convertToAdexTargetValue(value, mappingDefinition, logger);
|
|
30
|
+
return {
|
|
31
|
+
[mappingDefinition.attribute]: adexTargetValue
|
|
32
|
+
};
|
|
33
|
+
};
|
|
34
|
+
export const toAdexListType = (keyValueMap, mappingDefinition, logger) => {
|
|
35
|
+
const value = extractStringOrNumber(keyValueMap, 'string', mappingDefinition.key);
|
|
36
|
+
if ((value === undefined &&
|
|
37
|
+
(mappingDefinition.defaultValue === undefined ||
|
|
38
|
+
mappingDefinition.defaultValue.length === 0)) ||
|
|
39
|
+
typeof value === 'number') {
|
|
40
|
+
logger.warn('Adex DMP', `value for key "${mappingDefinition.key}" was empty or number. Value:`, value);
|
|
41
|
+
return undefined;
|
|
42
|
+
}
|
|
43
|
+
const adexTargetValue = convertToAdexListValue(value, mappingDefinition, logger);
|
|
44
|
+
return {
|
|
45
|
+
[mappingDefinition.attribute]: adexTargetValue
|
|
46
|
+
};
|
|
47
|
+
};
|
|
48
|
+
const sortAndToListObject = (arr) => Object.fromEntries(arr.sort().map(listValue => [listValue, 1]));
|
|
49
|
+
const sortAndJoin = (arr) => arr.sort().join(',');
|
|
50
|
+
const extractStringOrNumber = (keyValueMap, valueType, keyToExtract) => (valueType === 'number' ? Number(keyValueMap[keyToExtract]) : keyValueMap[keyToExtract]);
|
|
51
|
+
const convertToAdexTargetValue = (value, mappingDefinition, logger) => value !== undefined && !Number.isNaN(value)
|
|
52
|
+
?
|
|
53
|
+
Array.isArray(value)
|
|
54
|
+
?
|
|
55
|
+
sortAndJoin(value)
|
|
56
|
+
:
|
|
57
|
+
value
|
|
58
|
+
:
|
|
59
|
+
logAndUseDefaultValue(mappingDefinition, logger);
|
|
60
|
+
const convertToAdexListValue = (value, mappingDefinition, logger) => {
|
|
61
|
+
if (value === undefined) {
|
|
62
|
+
return logAndUseDefaultValue(mappingDefinition, logger);
|
|
63
|
+
}
|
|
64
|
+
return Array.isArray(value)
|
|
65
|
+
?
|
|
66
|
+
sortAndToListObject(value)
|
|
67
|
+
:
|
|
68
|
+
{ [value]: 1 };
|
|
69
|
+
};
|
|
70
|
+
const logAndUseDefaultValue = (mappingDefinition, logger) => {
|
|
71
|
+
logger.warn('Adex DMP', 'using defaultValue', mappingDefinition.defaultValue, 'as fallback for key', mappingDefinition.key);
|
|
72
|
+
return mappingDefinition.adexValueType === 'list'
|
|
73
|
+
? mappingDefinition.defaultValue
|
|
74
|
+
? sortAndToListObject(mappingDefinition.defaultValue)
|
|
75
|
+
: undefined
|
|
76
|
+
: mappingDefinition.defaultValue;
|
|
77
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export const trackUtiqId = (config, window__) => {
|
|
2
|
+
var _a;
|
|
3
|
+
window__.Utiq || (window__.Utiq = { queue: [] });
|
|
4
|
+
(_a = window__.Utiq).queue || (_a.queue = []);
|
|
5
|
+
window__.Utiq.queue.push(() => {
|
|
6
|
+
window__.Utiq?.API?.addEventListener('onIdsAvailable', ({ mtid }) => {
|
|
7
|
+
window__._adexc.push([
|
|
8
|
+
`/${config.adexCustomerId}/${config.adexTagId}/`,
|
|
9
|
+
'cm',
|
|
10
|
+
'_cm',
|
|
11
|
+
[308, mtid]
|
|
12
|
+
]);
|
|
13
|
+
});
|
|
14
|
+
});
|
|
15
|
+
};
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { mkConfigureStep, mkInitStep } from 'ad-tag/ads/adPipeline';
|
|
2
|
+
import { AssetLoadMethod } from 'ad-tag/util/assetLoaderService';
|
|
3
|
+
import { isNotNull } from 'ad-tag/util/arrayUtils';
|
|
4
|
+
import { sendAdvertisingID } from 'ad-tag/ads/modules/adex/sendAdvertisingId';
|
|
5
|
+
import { tcfapi } from 'ad-tag/types/tcfapi';
|
|
6
|
+
var TCPurpose = tcfapi.responses.TCPurpose;
|
|
7
|
+
import { toAdexListType, toAdexMapType, toAdexStringOrNumberType } from 'ad-tag/ads/modules/adex/adex-mapping';
|
|
8
|
+
import { trackUtiqId } from 'ad-tag/ads/modules/adex/adexUtiq';
|
|
9
|
+
export class AdexModule {
|
|
10
|
+
constructor() {
|
|
11
|
+
this.name = 'the-adex-dmp';
|
|
12
|
+
this.description = 'Moli DMP module for The Adex.';
|
|
13
|
+
this.moduleType = 'dmp';
|
|
14
|
+
this.adexConfig = null;
|
|
15
|
+
this.isLoaded = false;
|
|
16
|
+
this.hasRequiredConsent = (tcData) => !tcData.gdprApplies ||
|
|
17
|
+
(tcData.vendor.consents['44'] &&
|
|
18
|
+
[
|
|
19
|
+
TCPurpose.STORE_INFORMATION_ON_DEVICE,
|
|
20
|
+
TCPurpose.SELECT_BASIC_ADS,
|
|
21
|
+
TCPurpose.CREATE_PERSONALISED_ADS_PROFILE,
|
|
22
|
+
TCPurpose.SELECT_PERSONALISED_ADS,
|
|
23
|
+
TCPurpose.CREATE_PERSONALISED_CONTENT_PROFILE,
|
|
24
|
+
TCPurpose.SELECT_PERSONALISED_CONTENT,
|
|
25
|
+
TCPurpose.MEASURE_AD_PERFORMANCE,
|
|
26
|
+
TCPurpose.MEASURE_CONTENT_PERFORMANCE,
|
|
27
|
+
TCPurpose.APPLY_MARKET_RESEARCH,
|
|
28
|
+
TCPurpose.DEVELOP_IMPROVE_PRODUCTS
|
|
29
|
+
].every(purpose => tcData.purpose.consents[purpose]));
|
|
30
|
+
this.getOrInitAdexQueue = (_window) => {
|
|
31
|
+
const adexWindow = _window;
|
|
32
|
+
adexWindow._adexc = adexWindow._adexc || [];
|
|
33
|
+
return adexWindow;
|
|
34
|
+
};
|
|
35
|
+
this.getAdexKeyValues = (context, config) => {
|
|
36
|
+
const gamKeyValues = {
|
|
37
|
+
...context.config__.targeting?.keyValues,
|
|
38
|
+
...context.runtimeConfig__.keyValues
|
|
39
|
+
};
|
|
40
|
+
if (Object.keys(gamKeyValues).length > 0) {
|
|
41
|
+
return config.mappingDefinitions
|
|
42
|
+
.map(def => {
|
|
43
|
+
switch (def.adexValueType) {
|
|
44
|
+
case 'map':
|
|
45
|
+
return toAdexMapType(gamKeyValues, def, context.logger__);
|
|
46
|
+
case 'list':
|
|
47
|
+
return toAdexListType(gamKeyValues, def, context.logger__);
|
|
48
|
+
default:
|
|
49
|
+
return toAdexStringOrNumberType(gamKeyValues, def, context.logger__);
|
|
50
|
+
}
|
|
51
|
+
})
|
|
52
|
+
.filter(isNotNull);
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
this.configureAdexC = (context, config) => {
|
|
56
|
+
const adexWindow = this.getOrInitAdexQueue(context.window__);
|
|
57
|
+
const adexKeyValues = this.getAdexKeyValues(context, config);
|
|
58
|
+
if (!adexKeyValues) {
|
|
59
|
+
context.logger__.warn('Adex DMP', 'targeting key/values are empty');
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
adexWindow._adexc.push([
|
|
63
|
+
`/${config.adexCustomerId}/${config.adexTagId}/`,
|
|
64
|
+
'ut',
|
|
65
|
+
'_kv',
|
|
66
|
+
[
|
|
67
|
+
adexKeyValues.reduce((aggregator, additionalKeyValue) => ({ ...aggregator, ...additionalKeyValue }), {}),
|
|
68
|
+
config.spaMode ? 1 : 0
|
|
69
|
+
]
|
|
70
|
+
]);
|
|
71
|
+
}
|
|
72
|
+
if (config.enabledPartners?.includes('utiq')) {
|
|
73
|
+
trackUtiqId(config, context.window__);
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
config__() {
|
|
78
|
+
return this.adexConfig;
|
|
79
|
+
}
|
|
80
|
+
configure__(moduleConfig) {
|
|
81
|
+
if (moduleConfig?.adex && moduleConfig.adex.enabled) {
|
|
82
|
+
this.adexConfig = moduleConfig.adex;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
initSteps__() {
|
|
86
|
+
const config = this.adexConfig;
|
|
87
|
+
if (config) {
|
|
88
|
+
return [mkInitStep('DMP module setup', context => this.track(context, config))];
|
|
89
|
+
}
|
|
90
|
+
return [];
|
|
91
|
+
}
|
|
92
|
+
configureSteps__() {
|
|
93
|
+
const config = this.adexConfig;
|
|
94
|
+
if (config?.spaMode) {
|
|
95
|
+
return [
|
|
96
|
+
mkConfigureStep('DMP module setup', context => {
|
|
97
|
+
this.configureAdexC(context, config);
|
|
98
|
+
return Promise.resolve();
|
|
99
|
+
})
|
|
100
|
+
];
|
|
101
|
+
}
|
|
102
|
+
return [];
|
|
103
|
+
}
|
|
104
|
+
track(context, adexConfig) {
|
|
105
|
+
const { adexCustomerId, adexTagId, appConfig } = adexConfig;
|
|
106
|
+
this.configureAdexC(context, adexConfig);
|
|
107
|
+
const adexKeyValues = this.getAdexKeyValues(context, adexConfig);
|
|
108
|
+
const gamKeyValues = {
|
|
109
|
+
...context.config__.targeting?.keyValues,
|
|
110
|
+
...context.runtimeConfig__.keyValues
|
|
111
|
+
};
|
|
112
|
+
if (this.hasRequiredConsent(context.tcData__) &&
|
|
113
|
+
!this.isLoaded &&
|
|
114
|
+
Object.keys(gamKeyValues).length > 0) {
|
|
115
|
+
this.isLoaded = true;
|
|
116
|
+
const hasValidMobileKeyValues = isNotNull(appConfig) &&
|
|
117
|
+
isNotNull(gamKeyValues[appConfig.advertiserIdKey]) &&
|
|
118
|
+
(gamKeyValues[appConfig.clientTypeKey] === 'android' ||
|
|
119
|
+
gamKeyValues[appConfig.clientTypeKey] === 'ios');
|
|
120
|
+
if (appConfig?.advertiserIdKey && hasValidMobileKeyValues) {
|
|
121
|
+
const consentString = context.tcData__.gdprApplies ? context.tcData__.tcString : undefined;
|
|
122
|
+
const advertisingIdValue = gamKeyValues[appConfig.advertiserIdKey];
|
|
123
|
+
typeof advertisingIdValue === 'string' &&
|
|
124
|
+
adexKeyValues &&
|
|
125
|
+
sendAdvertisingID(adexCustomerId, appConfig.adexMobileTagId ? appConfig.adexMobileTagId : adexTagId, advertisingIdValue, adexKeyValues, gamKeyValues[appConfig.clientTypeKey] ?? '', context.window__.fetch, context.logger__, consentString);
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
context.assetLoaderService__
|
|
129
|
+
.loadScript({
|
|
130
|
+
name: this.name,
|
|
131
|
+
assetUrl: `https://dmp.theadex.com/d/${adexCustomerId}/${adexTagId}/s/adex.js`,
|
|
132
|
+
loadMethod: AssetLoadMethod.TAG
|
|
133
|
+
})
|
|
134
|
+
.catch(error => context.logger__.error('failed to load adex', error));
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return Promise.resolve();
|
|
138
|
+
}
|
|
139
|
+
prepareRequestAdsSteps__() {
|
|
140
|
+
return [];
|
|
141
|
+
}
|
|
142
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
const getAdvertisingIdIFAType = (clientType) => {
|
|
2
|
+
if (clientType === 'android') {
|
|
3
|
+
return 'aaid';
|
|
4
|
+
}
|
|
5
|
+
else if (clientType === 'ios') {
|
|
6
|
+
return 'idfa';
|
|
7
|
+
}
|
|
8
|
+
else {
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
export const sendAdvertisingID = (adexCustomerId, adexTagId, advertisingId, adexAttributes, clientType, fetch, logger, consentString) => {
|
|
13
|
+
const ifaType = getAdvertisingIdIFAType(typeof clientType === 'string' ? clientType : clientType[0]);
|
|
14
|
+
const keyValuesMap = adexAttributes.reduce((acc, currentValue) => {
|
|
15
|
+
return { ...acc, ...currentValue };
|
|
16
|
+
}, {});
|
|
17
|
+
const keyValuesParameter = !!adexAttributes.length ? `&kv=${JSON.stringify(keyValuesMap)}` : '';
|
|
18
|
+
const consentParameter = consentString ? `&gdpr_consent=${consentString}` : '';
|
|
19
|
+
fetch(`https://api.theadex.com/collector/v1/ifa/c/${adexCustomerId}/t/${adexTagId}/request?&ifa=${advertisingId}&ifa_type=${ifaType}${keyValuesParameter}${consentParameter}`).catch(error => logger.error(error));
|
|
20
|
+
};
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { mkConfigureStep, mkInitStep } from '../../adPipeline';
|
|
2
|
+
const matches = (href, log) => (entry) => {
|
|
3
|
+
switch (entry.matchType) {
|
|
4
|
+
case 'exact':
|
|
5
|
+
return href === entry.pattern;
|
|
6
|
+
case 'contains':
|
|
7
|
+
return href.indexOf(entry.pattern) > -1;
|
|
8
|
+
case 'regex':
|
|
9
|
+
try {
|
|
10
|
+
return RegExp(entry.pattern).test(href);
|
|
11
|
+
}
|
|
12
|
+
catch (e) {
|
|
13
|
+
log.error(`Invalid regex pattern: ${entry.pattern}`, e);
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
default:
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
export const isBlocklisted = (blocklist, href, log) => blocklist.urls.some(matches(href, log));
|
|
21
|
+
export const createBlocklistedUrls = () => {
|
|
22
|
+
let blocklistConfig = null;
|
|
23
|
+
let blocklistCache = null;
|
|
24
|
+
const config__ = () => blocklistConfig;
|
|
25
|
+
const configure__ = (moduleConfig) => {
|
|
26
|
+
if (moduleConfig?.blocklist && moduleConfig.blocklist.enabled) {
|
|
27
|
+
blocklistConfig = moduleConfig.blocklist;
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
const getBlocklist = (blocklist, assetLoaderService, log) => {
|
|
31
|
+
switch (blocklist.provider) {
|
|
32
|
+
case 'static':
|
|
33
|
+
return () => Promise.resolve(blocklist.blocklist);
|
|
34
|
+
case 'dynamic':
|
|
35
|
+
let cachedResult;
|
|
36
|
+
return () => {
|
|
37
|
+
if (!cachedResult) {
|
|
38
|
+
cachedResult = loadConfigWithRetry(assetLoaderService, blocklist.endpoint, 3).catch(e => {
|
|
39
|
+
log.error('Blocklist URLs', 'Fetching blocklisted urls failed', e);
|
|
40
|
+
return { urls: [] };
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
return cachedResult;
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
const loadConfigWithRetry = (assetLoaderService, endpoint, retriesLeft, lastError = null) => {
|
|
48
|
+
if (retriesLeft <= 0) {
|
|
49
|
+
return Promise.reject(lastError);
|
|
50
|
+
}
|
|
51
|
+
if (!blocklistCache) {
|
|
52
|
+
blocklistCache = assetLoaderService
|
|
53
|
+
.loadJson('blocklist-urls.json', endpoint)
|
|
54
|
+
.catch(error => {
|
|
55
|
+
const exponentialBackoff = new Promise(resolve => setTimeout(resolve, 100 / retriesLeft));
|
|
56
|
+
return exponentialBackoff.then(() => loadConfigWithRetry(assetLoaderService, endpoint, retriesLeft - 1, error));
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
return blocklistCache;
|
|
60
|
+
};
|
|
61
|
+
const initSteps__ = () => {
|
|
62
|
+
return blocklistConfig
|
|
63
|
+
? [
|
|
64
|
+
mkInitStep('blocklist-urls-init', ctx => {
|
|
65
|
+
switch (blocklistConfig?.mode) {
|
|
66
|
+
case 'key-value':
|
|
67
|
+
const key = blocklistConfig.key;
|
|
68
|
+
const value = blocklistConfig.isBlocklistedValue || 'true';
|
|
69
|
+
return getBlocklist(blocklistConfig.blocklist, ctx.assetLoaderService__, ctx.logger__)().then(blocklist => {
|
|
70
|
+
if (isBlocklisted(blocklist, ctx.window__.location.href, ctx.logger__)) {
|
|
71
|
+
ctx.window__.googletag
|
|
72
|
+
.pubads()
|
|
73
|
+
.setTargeting(key, value);
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
case 'block':
|
|
77
|
+
return getBlocklist(blocklistConfig.blocklist, ctx.assetLoaderService__, ctx.logger__)().then(blocklist => {
|
|
78
|
+
ctx.logger__.debug('Blocklist URLs', 'using blocklist', blocklist);
|
|
79
|
+
if (isBlocklisted(blocklist, ctx.window__.location.href, ctx.logger__)) {
|
|
80
|
+
return Promise.reject('blocklisted url found. Abort ad pipeline run');
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
default:
|
|
84
|
+
return Promise.resolve();
|
|
85
|
+
}
|
|
86
|
+
})
|
|
87
|
+
]
|
|
88
|
+
: [];
|
|
89
|
+
};
|
|
90
|
+
const configureSteps__ = () => {
|
|
91
|
+
const config = blocklistConfig;
|
|
92
|
+
return config
|
|
93
|
+
? [
|
|
94
|
+
mkConfigureStep('blocklist-labels', async (ctx) => {
|
|
95
|
+
const loadedBlocklist = await getBlocklist(config.blocklist, ctx.assetLoaderService__, ctx.logger__)();
|
|
96
|
+
const matcher = matches(ctx.window__.location.href, ctx.logger__);
|
|
97
|
+
loadedBlocklist.labels?.forEach(entry => {
|
|
98
|
+
const result = entry.reverseMatch === true ? !matcher(entry) : matcher(entry);
|
|
99
|
+
if (result) {
|
|
100
|
+
ctx.labelConfigService__.addLabel(entry.label);
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
})
|
|
104
|
+
]
|
|
105
|
+
: [];
|
|
106
|
+
};
|
|
107
|
+
const prepareRequestAdsSteps__ = () => [];
|
|
108
|
+
return {
|
|
109
|
+
name: 'Blocklist URLs',
|
|
110
|
+
description: '',
|
|
111
|
+
moduleType: 'policy',
|
|
112
|
+
config__,
|
|
113
|
+
configure__,
|
|
114
|
+
initSteps__,
|
|
115
|
+
configureSteps__,
|
|
116
|
+
prepareRequestAdsSteps__
|
|
117
|
+
};
|
|
118
|
+
};
|