@streamlayer/feature-advertisement 1.1.1-rc.1350 → 1.1.1-rc.3.pda7fd93b
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/lib/advertisement.js +24 -9
- package/lib/cjs/advertisement.js +24 -9
- package/lib/cjs/index.d.ts +2 -0
- package/lib/cjs/index.js +2 -0
- package/lib/cjs/units/ad-macros.d.ts +7 -0
- package/lib/cjs/units/ad-macros.js +91 -0
- package/lib/cjs/units/external-ad.d.ts +69 -0
- package/lib/cjs/units/external-ad.js +93 -62
- package/lib/cjs/units/external-pause-ad.js +7 -1
- package/lib/cjs/units/gam.d.ts +11 -1
- package/lib/cjs/units/gam.js +65 -17
- package/lib/cjs/units/parse-vast-extensions.d.ts +10 -1
- package/lib/cjs/units/parse-vast-extensions.js +5 -1
- package/lib/cjs/units/sponsor-logo.d.ts +78 -0
- package/lib/cjs/units/sponsor-logo.js +194 -0
- package/lib/cjs/units/ssai-detector.js +27 -11
- package/lib/cjs/units/ssai-log.d.ts +16 -0
- package/lib/cjs/units/ssai-log.js +23 -0
- package/lib/cjs/units/ssai-providers.js +48 -11
- package/lib/classic/advertisement.js +34 -17
- package/lib/classic/cjs/advertisement.js +34 -17
- package/lib/classic/cjs/index.js +2 -0
- package/lib/classic/cjs/units/ad-macros.js +115 -0
- package/lib/classic/cjs/units/external-ad.js +111 -64
- package/lib/classic/cjs/units/external-pause-ad.js +5 -1
- package/lib/classic/cjs/units/gam.js +73 -10
- package/lib/classic/cjs/units/parse-vast-extensions.js +5 -1
- package/lib/classic/cjs/units/sponsor-logo.js +201 -0
- package/lib/classic/cjs/units/ssai-detector.js +33 -8
- package/lib/classic/cjs/units/ssai-log.js +23 -0
- package/lib/classic/cjs/units/ssai-providers.js +86 -24
- package/lib/classic/index.js +1 -0
- package/lib/classic/units/ad-macros.js +113 -0
- package/lib/classic/units/external-ad.js +109 -65
- package/lib/classic/units/external-pause-ad.js +5 -1
- package/lib/classic/units/gam.js +73 -10
- package/lib/classic/units/parse-vast-extensions.js +5 -1
- package/lib/classic/units/sponsor-logo.js +197 -0
- package/lib/classic/units/ssai-detector.js +33 -8
- package/lib/classic/units/ssai-log.js +21 -0
- package/lib/classic/units/ssai-providers.js +86 -24
- package/lib/index.d.ts +2 -0
- package/lib/index.js +1 -0
- package/lib/units/ad-macros.d.ts +7 -0
- package/lib/units/ad-macros.js +87 -0
- package/lib/units/external-ad.d.ts +69 -0
- package/lib/units/external-ad.js +92 -64
- package/lib/units/external-pause-ad.js +7 -1
- package/lib/units/gam.d.ts +11 -1
- package/lib/units/gam.js +65 -17
- package/lib/units/parse-vast-extensions.d.ts +10 -1
- package/lib/units/parse-vast-extensions.js +5 -1
- package/lib/units/sponsor-logo.d.ts +78 -0
- package/lib/units/sponsor-logo.js +188 -0
- package/lib/units/ssai-detector.js +27 -11
- package/lib/units/ssai-log.d.ts +16 -0
- package/lib/units/ssai-log.js +18 -0
- package/lib/units/ssai-providers.js +48 -11
- package/lib/vast-client.d.ts +9 -0
- package/package.json +8 -8
|
@@ -2,9 +2,55 @@
|
|
|
2
2
|
|
|
3
3
|
var sdkWebLogger = require('@streamlayer/sdk-web-logger');
|
|
4
4
|
var vastClient = require('@dailymotion/vast-client');
|
|
5
|
+
var ssaiLog = require('./ssai-log.js');
|
|
5
6
|
|
|
6
7
|
const log = sdkWebLogger.createLogger('ssai:provider');
|
|
7
8
|
const DEFAULT_CREATIVE_API_BASE = 'https://hls.next.streamlayer.io/api/creatives/';
|
|
9
|
+
/**
|
|
10
|
+
* One creative-metadata round trip, on the narrative log: the request the SDK
|
|
11
|
+
* made and the body the ad service answered with, verbatim. This is the half of
|
|
12
|
+
* the story the manifest cannot tell — the ad's identity comes from the playout,
|
|
13
|
+
* its content comes from here.
|
|
14
|
+
*/
|
|
15
|
+
const fetchCreativeJson = async (providerId, url, signal) => {
|
|
16
|
+
// `adApiBase` is customer infrastructure and may carry a key in its query;
|
|
17
|
+
// the request uses the URL as given, the log gets it with such values masked.
|
|
18
|
+
const shown = maskUrlSecrets(url);
|
|
19
|
+
ssaiLog.ssaiq.info({ provider: providerId, url: shown }, `[GET] → ${shown}`);
|
|
20
|
+
let response;
|
|
21
|
+
try {
|
|
22
|
+
response = await fetch(url, { signal });
|
|
23
|
+
}
|
|
24
|
+
catch (error) {
|
|
25
|
+
// An abort is the SDK cancelling its own request (break ended, creative
|
|
26
|
+
// changed) — a note, not a failure.
|
|
27
|
+
if (error?.name === 'AbortError') {
|
|
28
|
+
ssaiLog.ssaiq.debug({ provider: providerId, url: shown }, `[GET] ✗ aborted ${shown}`);
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
ssaiLog.ssaiq.warn({ provider: providerId, url: shown, error: String(error) }, `[GET] ✗ ${shown}`);
|
|
32
|
+
}
|
|
33
|
+
throw error;
|
|
34
|
+
}
|
|
35
|
+
if (!response.ok) {
|
|
36
|
+
ssaiLog.ssaiq.warn({ provider: providerId, url: shown, status: response.status }, `[GET] ✗ ${response.status} ${shown}`);
|
|
37
|
+
throw new Error(`${providerId} creative request failed: ${response.status}`);
|
|
38
|
+
}
|
|
39
|
+
let raw;
|
|
40
|
+
try {
|
|
41
|
+
raw = (await response.json());
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
// A 200 with a body that is not JSON would otherwise leave the log looking
|
|
45
|
+
// like a request still in flight.
|
|
46
|
+
ssaiLog.ssaiq.warn({ provider: providerId, url: shown, error: String(error) }, `[GET] ✗ ${response.status} ${shown} — not JSON`);
|
|
47
|
+
throw error;
|
|
48
|
+
}
|
|
49
|
+
ssaiLog.ssaiq.info({ provider: providerId, status: response.status, body: raw }, `[GET] ← ${response.status} ad metadata — ${raw?.advertiser || raw?.title || raw?.creativeId || 'ad'}`);
|
|
50
|
+
return raw;
|
|
51
|
+
};
|
|
52
|
+
/** `?apiKey=…` → `?apiKey=«redacted»`, for URLs that go on a screen. Regex, not `URL`: this ships to 2016 TV engines. */
|
|
53
|
+
const maskUrlSecrets = (url) => url.replace(/([?&][^=&]*(?:token|secret|password|key|authorization)[^=&]*=)[^&]*/gi, '$1«redacted»');
|
|
8
54
|
/**
|
|
9
55
|
* Shared low-level beacon helper. Prefers `navigator.sendBeacon` and falls back
|
|
10
56
|
* to a no-cors `fetch`. Used by the default impression path and available to
|
|
@@ -103,12 +149,7 @@ const createStreamLayerProvider = (config = {}) => {
|
|
|
103
149
|
return {
|
|
104
150
|
id: 'streamlayer',
|
|
105
151
|
resolve: async (creativeId, signal) => {
|
|
106
|
-
const
|
|
107
|
-
const response = await fetch(url, { signal });
|
|
108
|
-
if (!response.ok) {
|
|
109
|
-
throw new Error(`creative request failed: ${response.status}`);
|
|
110
|
-
}
|
|
111
|
-
const raw = (await response.json());
|
|
152
|
+
const raw = await fetchCreativeJson('streamlayer', buildCreativeUrl(creativeApiBase, creativeId), signal);
|
|
112
153
|
return mapApiResponse({ ...raw, creativeId: raw.creativeId || creativeId });
|
|
113
154
|
},
|
|
114
155
|
fireImpression: fireDefaultImpression,
|
|
@@ -152,11 +193,7 @@ const createQuortexProvider = (config = {}) => {
|
|
|
152
193
|
if (!adApiBase) {
|
|
153
194
|
throw new Error('quortex provider is missing `adApiBase`');
|
|
154
195
|
}
|
|
155
|
-
const
|
|
156
|
-
if (!response.ok) {
|
|
157
|
-
throw new Error(`quortex creative request failed: ${response.status}`);
|
|
158
|
-
}
|
|
159
|
-
const raw = (await response.json());
|
|
196
|
+
const raw = await fetchCreativeJson('quortex', buildQuortexUrl(adApiBase, tenantName, adId), signal);
|
|
160
197
|
return mapApiResponse({ ...raw, creativeId: raw.creativeId || adId });
|
|
161
198
|
},
|
|
162
199
|
fireImpression: fireDefaultImpression,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { defineProperty as _defineProperty, objectSpread2 as _objectSpread2 } from './_virtual/_rollupPluginBabelHelpers.js';
|
|
2
2
|
import 'core-js/modules/es.array.iterator.js';
|
|
3
3
|
import 'core-js/modules/esnext.iterator.constructor.js';
|
|
4
|
+
import 'core-js/modules/esnext.iterator.find.js';
|
|
4
5
|
import 'core-js/modules/esnext.iterator.for-each.js';
|
|
5
6
|
import 'core-js/modules/esnext.set.difference.v2.js';
|
|
6
7
|
import 'core-js/modules/esnext.set.intersection.v2.js';
|
|
@@ -20,6 +21,7 @@ import { SsaiCreativeCache } from './units/ssai-creative.js';
|
|
|
20
21
|
import { resolveProvider } from './units/ssai-providers.js';
|
|
21
22
|
import { createSsaiDetector } from './units/ssai-detector.js';
|
|
22
23
|
import { createSsaiAdBreakStore } from './units/ssai-ad-break.js';
|
|
24
|
+
import { ssaiq } from './units/ssai-log.js';
|
|
23
25
|
import { preloadCreativeAssets } from './units/ssai-preload.js';
|
|
24
26
|
|
|
25
27
|
class Advertisement extends AbstractFeatureUnit {
|
|
@@ -165,10 +167,33 @@ class Advertisement extends AbstractFeatureUnit {
|
|
|
165
167
|
const provider = resolveProvider(options.provider);
|
|
166
168
|
const cache = new SsaiCreativeCache(provider);
|
|
167
169
|
this.ssaiCache = cache;
|
|
170
|
+
/**
|
|
171
|
+
* Every beacon the SDK fires gets a line, with the URL it was fired at.
|
|
172
|
+
* The narrative log is read as a live trace of one ad, and "we called the
|
|
173
|
+
* ad server's own tracking URL" is as much of the story as the request
|
|
174
|
+
* that fetched the creative in the first place.
|
|
175
|
+
*/
|
|
176
|
+
const logBeacon = (creativeId, event) => {
|
|
177
|
+
var _cache$peek;
|
|
178
|
+
const url = (_cache$peek = cache.peek(creativeId)) === null || _cache$peek === void 0 || (_cache$peek = _cache$peek.trackingEvents) === null || _cache$peek === void 0 || (_cache$peek = _cache$peek.find(entry => entry.event === event)) === null || _cache$peek === void 0 ? void 0 : _cache$peek.url;
|
|
179
|
+
ssaiq.info({
|
|
180
|
+
creativeId,
|
|
181
|
+
event,
|
|
182
|
+
url
|
|
183
|
+
}, `[beacon] ${event} fired — ${creativeId}`);
|
|
184
|
+
};
|
|
168
185
|
this.fireSsaiImpression = creativeId => {
|
|
186
|
+
var _cache$peek2;
|
|
187
|
+
// The UI only calls this once the bar is actually rendered, so this is
|
|
188
|
+
// the moment the viewer saw the ad — not the moment it resolved.
|
|
189
|
+
ssaiq.info({
|
|
190
|
+
creativeId
|
|
191
|
+
}, `[ad] ad on screen — ${((_cache$peek2 = cache.peek(creativeId)) === null || _cache$peek2 === void 0 ? void 0 : _cache$peek2.advertiser) || creativeId}`);
|
|
192
|
+
logBeacon(creativeId, 'impression');
|
|
169
193
|
cache.fireImpressionById(creativeId);
|
|
170
194
|
};
|
|
171
195
|
this.fireSsaiTracking = (creativeId, event) => {
|
|
196
|
+
logBeacon(creativeId, event);
|
|
172
197
|
cache.fireTrackingById(creativeId, event);
|
|
173
198
|
};
|
|
174
199
|
this.fireSsaiCompanionClick = creativeId => {
|
|
@@ -186,10 +211,14 @@ class Advertisement extends AbstractFeatureUnit {
|
|
|
186
211
|
* when the ad actually starts.
|
|
187
212
|
*/
|
|
188
213
|
const prefetchCreative = creativeId => {
|
|
189
|
-
cache.resolve(creativeId).then(creative => preloadCreativeAssets(creative)
|
|
214
|
+
cache.resolve(creativeId).then(creative => preloadCreativeAssets(creative).then(() => creative)).then(creative => {
|
|
190
215
|
preloadedIds.add(creativeId);
|
|
191
|
-
|
|
192
|
-
|
|
216
|
+
ssaiq.info({
|
|
217
|
+
creativeId
|
|
218
|
+
}, `[ad] creative ready — ${creative.advertiser} (artwork preloaded)`);
|
|
219
|
+
}).catch(() => {
|
|
220
|
+
// Already on `ssaiq` from the provider; the break may still be
|
|
221
|
+
// minutes away, and `loadCreative` retries when it actually starts.
|
|
193
222
|
});
|
|
194
223
|
};
|
|
195
224
|
const publishReady = (range, creative) => {
|
|
@@ -251,15 +280,9 @@ class Advertisement extends AbstractFeatureUnit {
|
|
|
251
280
|
videoRef: options.videoRef,
|
|
252
281
|
manifestUrl: options.manifestUrl,
|
|
253
282
|
onAdDiscovered: creativeId => {
|
|
254
|
-
this.log.debug({
|
|
255
|
-
creativeId
|
|
256
|
-
}, 'ssai ad discovered, prefetching creative');
|
|
257
283
|
prefetchCreative(creativeId);
|
|
258
284
|
},
|
|
259
285
|
onBreakStart: range => {
|
|
260
|
-
this.log.debug({
|
|
261
|
-
creativeId: range.creativeId
|
|
262
|
-
}, 'ssai break start');
|
|
263
286
|
eventBus.emit('ssaiAdBreak', {
|
|
264
287
|
action: 'breakStart',
|
|
265
288
|
payload: {
|
|
@@ -269,9 +292,6 @@ class Advertisement extends AbstractFeatureUnit {
|
|
|
269
292
|
loadCreative(range);
|
|
270
293
|
},
|
|
271
294
|
onCreativeChange: range => {
|
|
272
|
-
this.log.debug({
|
|
273
|
-
creativeId: range.creativeId
|
|
274
|
-
}, 'ssai creative change');
|
|
275
295
|
loadCreative(range);
|
|
276
296
|
},
|
|
277
297
|
onAdEnding: range => {
|
|
@@ -286,14 +306,11 @@ class Advertisement extends AbstractFeatureUnit {
|
|
|
286
306
|
}));
|
|
287
307
|
},
|
|
288
308
|
onProgress: (range, event) => {
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
event
|
|
292
|
-
}, 'ssai progress');
|
|
309
|
+
// Logged by the detector as `progress:<event>` at the moment playback
|
|
310
|
+
// crosses the marker; nothing to add here.
|
|
293
311
|
cache.fireTrackingById(range.creativeId, event);
|
|
294
312
|
},
|
|
295
313
|
onBreakEnd: () => {
|
|
296
|
-
this.log.debug('ssai break end');
|
|
297
314
|
const current = this.ssaiAdBreak.get();
|
|
298
315
|
eventBus.emit('ssaiAdBreak', {
|
|
299
316
|
action: 'breakEnd',
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
var _rollupPluginBabelHelpers = require('./_virtual/_rollupPluginBabelHelpers.js');
|
|
4
4
|
require('core-js/modules/es.array.iterator.js');
|
|
5
5
|
require('core-js/modules/esnext.iterator.constructor.js');
|
|
6
|
+
require('core-js/modules/esnext.iterator.find.js');
|
|
6
7
|
require('core-js/modules/esnext.iterator.for-each.js');
|
|
7
8
|
require('core-js/modules/esnext.set.difference.v2.js');
|
|
8
9
|
require('core-js/modules/esnext.set.intersection.v2.js');
|
|
@@ -22,6 +23,7 @@ var ssaiCreative = require('./units/ssai-creative.js');
|
|
|
22
23
|
var ssaiProviders = require('./units/ssai-providers.js');
|
|
23
24
|
var ssaiDetector = require('./units/ssai-detector.js');
|
|
24
25
|
var ssaiAdBreak = require('./units/ssai-ad-break.js');
|
|
26
|
+
var ssaiLog = require('./units/ssai-log.js');
|
|
25
27
|
var ssaiPreload = require('./units/ssai-preload.js');
|
|
26
28
|
|
|
27
29
|
class Advertisement extends classic.AbstractFeatureUnit {
|
|
@@ -167,10 +169,33 @@ class Advertisement extends classic.AbstractFeatureUnit {
|
|
|
167
169
|
const provider = ssaiProviders.resolveProvider(options.provider);
|
|
168
170
|
const cache = new ssaiCreative.SsaiCreativeCache(provider);
|
|
169
171
|
this.ssaiCache = cache;
|
|
172
|
+
/**
|
|
173
|
+
* Every beacon the SDK fires gets a line, with the URL it was fired at.
|
|
174
|
+
* The narrative log is read as a live trace of one ad, and "we called the
|
|
175
|
+
* ad server's own tracking URL" is as much of the story as the request
|
|
176
|
+
* that fetched the creative in the first place.
|
|
177
|
+
*/
|
|
178
|
+
const logBeacon = (creativeId, event) => {
|
|
179
|
+
var _cache$peek;
|
|
180
|
+
const url = (_cache$peek = cache.peek(creativeId)) === null || _cache$peek === void 0 || (_cache$peek = _cache$peek.trackingEvents) === null || _cache$peek === void 0 || (_cache$peek = _cache$peek.find(entry => entry.event === event)) === null || _cache$peek === void 0 ? void 0 : _cache$peek.url;
|
|
181
|
+
ssaiLog.ssaiq.info({
|
|
182
|
+
creativeId,
|
|
183
|
+
event,
|
|
184
|
+
url
|
|
185
|
+
}, `[beacon] ${event} fired — ${creativeId}`);
|
|
186
|
+
};
|
|
170
187
|
this.fireSsaiImpression = creativeId => {
|
|
188
|
+
var _cache$peek2;
|
|
189
|
+
// The UI only calls this once the bar is actually rendered, so this is
|
|
190
|
+
// the moment the viewer saw the ad — not the moment it resolved.
|
|
191
|
+
ssaiLog.ssaiq.info({
|
|
192
|
+
creativeId
|
|
193
|
+
}, `[ad] ad on screen — ${((_cache$peek2 = cache.peek(creativeId)) === null || _cache$peek2 === void 0 ? void 0 : _cache$peek2.advertiser) || creativeId}`);
|
|
194
|
+
logBeacon(creativeId, 'impression');
|
|
171
195
|
cache.fireImpressionById(creativeId);
|
|
172
196
|
};
|
|
173
197
|
this.fireSsaiTracking = (creativeId, event) => {
|
|
198
|
+
logBeacon(creativeId, event);
|
|
174
199
|
cache.fireTrackingById(creativeId, event);
|
|
175
200
|
};
|
|
176
201
|
this.fireSsaiCompanionClick = creativeId => {
|
|
@@ -188,10 +213,14 @@ class Advertisement extends classic.AbstractFeatureUnit {
|
|
|
188
213
|
* when the ad actually starts.
|
|
189
214
|
*/
|
|
190
215
|
const prefetchCreative = creativeId => {
|
|
191
|
-
cache.resolve(creativeId).then(creative => ssaiPreload.preloadCreativeAssets(creative)
|
|
216
|
+
cache.resolve(creativeId).then(creative => ssaiPreload.preloadCreativeAssets(creative).then(() => creative)).then(creative => {
|
|
192
217
|
preloadedIds.add(creativeId);
|
|
193
|
-
|
|
194
|
-
|
|
218
|
+
ssaiLog.ssaiq.info({
|
|
219
|
+
creativeId
|
|
220
|
+
}, `[ad] creative ready — ${creative.advertiser} (artwork preloaded)`);
|
|
221
|
+
}).catch(() => {
|
|
222
|
+
// Already on `ssaiq` from the provider; the break may still be
|
|
223
|
+
// minutes away, and `loadCreative` retries when it actually starts.
|
|
195
224
|
});
|
|
196
225
|
};
|
|
197
226
|
const publishReady = (range, creative) => {
|
|
@@ -253,15 +282,9 @@ class Advertisement extends classic.AbstractFeatureUnit {
|
|
|
253
282
|
videoRef: options.videoRef,
|
|
254
283
|
manifestUrl: options.manifestUrl,
|
|
255
284
|
onAdDiscovered: creativeId => {
|
|
256
|
-
this.log.debug({
|
|
257
|
-
creativeId
|
|
258
|
-
}, 'ssai ad discovered, prefetching creative');
|
|
259
285
|
prefetchCreative(creativeId);
|
|
260
286
|
},
|
|
261
287
|
onBreakStart: range => {
|
|
262
|
-
this.log.debug({
|
|
263
|
-
creativeId: range.creativeId
|
|
264
|
-
}, 'ssai break start');
|
|
265
288
|
classic.eventBus.emit('ssaiAdBreak', {
|
|
266
289
|
action: 'breakStart',
|
|
267
290
|
payload: {
|
|
@@ -271,9 +294,6 @@ class Advertisement extends classic.AbstractFeatureUnit {
|
|
|
271
294
|
loadCreative(range);
|
|
272
295
|
},
|
|
273
296
|
onCreativeChange: range => {
|
|
274
|
-
this.log.debug({
|
|
275
|
-
creativeId: range.creativeId
|
|
276
|
-
}, 'ssai creative change');
|
|
277
297
|
loadCreative(range);
|
|
278
298
|
},
|
|
279
299
|
onAdEnding: range => {
|
|
@@ -288,14 +308,11 @@ class Advertisement extends classic.AbstractFeatureUnit {
|
|
|
288
308
|
}));
|
|
289
309
|
},
|
|
290
310
|
onProgress: (range, event) => {
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
event
|
|
294
|
-
}, 'ssai progress');
|
|
311
|
+
// Logged by the detector as `progress:<event>` at the moment playback
|
|
312
|
+
// crosses the marker; nothing to add here.
|
|
295
313
|
cache.fireTrackingById(range.creativeId, event);
|
|
296
314
|
},
|
|
297
315
|
onBreakEnd: () => {
|
|
298
|
-
this.log.debug('ssai break end');
|
|
299
316
|
const current = this.ssaiAdBreak.get();
|
|
300
317
|
classic.eventBus.emit('ssaiAdBreak', {
|
|
301
318
|
action: 'breakEnd',
|
package/lib/classic/cjs/index.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
var advertisement$1 = require('./advertisement.js');
|
|
4
4
|
var externalPauseAd = require('./units/external-pause-ad.js');
|
|
5
5
|
var pauseAdCard = require('./units/pause-ad-card.js');
|
|
6
|
+
var sponsorLogo = require('./units/sponsor-logo.js');
|
|
6
7
|
var gam = require('./units/gam.js');
|
|
7
8
|
var vastTracker = require('./units/vast-tracker.js');
|
|
8
9
|
require('core-js/modules/es.array.iterator.js');
|
|
@@ -29,6 +30,7 @@ const advertisement = (instance, _opts, done) => {
|
|
|
29
30
|
|
|
30
31
|
exports.isFullbleedVideoTemplate = externalPauseAd.isFullbleedVideoTemplate;
|
|
31
32
|
exports.INSIGHT_BODY_MAX_LENGTH = pauseAdCard.INSIGHT_BODY_MAX_LENGTH;
|
|
33
|
+
exports.selectSponsorLogo = sponsorLogo.selectSponsorLogo;
|
|
32
34
|
exports.gam = gam.gam;
|
|
33
35
|
exports.createCompanionTracker = vastTracker.createCompanionTracker;
|
|
34
36
|
exports.createVastTracker = vastTracker.createVastTracker;
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
require('core-js/modules/es.regexp.exec.js');
|
|
4
|
+
require('core-js/modules/es.string.replace.js');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Ad-tag macro substitution, for every ad server — not just GAM.
|
|
8
|
+
*
|
|
9
|
+
* Studio stores a programmatic tag verbatim (`gamRawUrl`, returned untouched by
|
|
10
|
+
* `processGamAdvertisement`), so whatever macros the ad server's own tag builder emitted reach the
|
|
11
|
+
* SDK literal. `gam()` used to fill only the GAM `[placeholder]` token, and only for
|
|
12
|
+
* `pubads.g.doubleclick.net` — every other vendor's tag went out with its macros unexpanded.
|
|
13
|
+
*
|
|
14
|
+
* That is not inert. Measured against the ADITION tag on event 11089
|
|
15
|
+
* (`…/banner?…&ts=[timestamp]&clickurl=[clickmacro]`): ADITION prepends the value of `clickurl` to
|
|
16
|
+
* its own redi URL, so a literal `[clickmacro]` came back inside `<CompanionClickThrough>` as
|
|
17
|
+
* `[clickmacro]https%3A%2F%2Fad1…` — a broken click-through. Sending `clickurl=` empty answers with
|
|
18
|
+
* a valid redi URL.
|
|
19
|
+
*
|
|
20
|
+
* **Two sources.** The cache buster is the SDK's own. The consent family — `${GDPR}` and
|
|
21
|
+
* `${GDPR_CONSENT_<gvlid>}`, IAB TCF, where the id is the vendor's Global Vendor List entry (39 is
|
|
22
|
+
* ADITION / Virtual Minds) — and the click macro come from the host app (`AdTagParams`, set through
|
|
23
|
+
* the `adTagParams` option or `sdk.setAdTagParams`). The SDK never reads a CMP and never guesses
|
|
24
|
+
* consent: a value the host did not pass goes out EMPTY, which ad servers read as "unknown" — never
|
|
25
|
+
* as `gdpr=0`. Empty rather than literal because a literal is not inert either: ADITION copied
|
|
26
|
+
* `${GDPR}` into its own redirect URL (`gdpr%3D%24%7BGDPR%7D`). See
|
|
27
|
+
* `docs/wiki/programmatic-ads-macros.md`.
|
|
28
|
+
*
|
|
29
|
+
* An unknown macro is left exactly as configured, which is what keeps the GAM `[placeholder]`
|
|
30
|
+
* tokens for the PAL path (`gam.ts` fills those per parameter, not per token).
|
|
31
|
+
*/
|
|
32
|
+
/**
|
|
33
|
+
* All three spellings in the wild — `[NAME]` (IAB / SpotX), `${NAME}` (Google) and bare `{NAME}` —
|
|
34
|
+
* each with its delimiters either literal or PERCENT-ENCODED.
|
|
35
|
+
*
|
|
36
|
+
* Neither half of that is theoretical. ADITION emitted `[timestamp]` / `[clickmacro]` for the
|
|
37
|
+
* campaign on event 11089 and `{CACHEBUSTER}` / `{GDPR}` for the one on 6203 — same ad server, same
|
|
38
|
+
* advertiser, different tag builder. And Studio stores the 6203 tag as `ts=%7BCACHEBUSTER%7D`, so
|
|
39
|
+
* matching only literal braces made the whole substitution a silent no-op on it: an unexpanded
|
|
40
|
+
* macro raises no error and the ad server still answers 200. `searchParams.get()` decodes on read,
|
|
41
|
+
* which is what hides the encoding from anyone inspecting the parsed URL.
|
|
42
|
+
*
|
|
43
|
+
* The `$` is optional rather than a fourth branch — an unowned `${NAME}` still returns its full
|
|
44
|
+
* matched text, `$` included, so the brace forms cannot corrupt each other.
|
|
45
|
+
*/
|
|
46
|
+
const MACRO_PATTERN = /(?:\[|%5B)([a-z0-9_]+)(?:\]|%5D)|(?:\$|%24)?(?:\{|%7B)([a-z0-9_]+)(?:\}|%7D)/gi;
|
|
47
|
+
/**
|
|
48
|
+
* Every host value lands inside a query-string VALUE, so it is encoded exactly once. That matters
|
|
49
|
+
* most for the click URL, which carries its own `?`/`&`: sent raw it re-opens the tag's query, the
|
|
50
|
+
* way the malformed 6203 tag did (`clickurl` truncated, every following parameter duplicated).
|
|
51
|
+
*/
|
|
52
|
+
const hostValue = value => value ? encodeURIComponent(value) : '';
|
|
53
|
+
const gdprConsent = ({
|
|
54
|
+
params
|
|
55
|
+
}) => hostValue(params === null || params === void 0 ? void 0 : params.tcString);
|
|
56
|
+
/**
|
|
57
|
+
* Macro name (lower-cased) → the value this SDK sends for it.
|
|
58
|
+
*
|
|
59
|
+
* `clickurl` / `clickmacro` take the host's click tracker, which the ad server PREPENDS to its own
|
|
60
|
+
* redirect — so the viewer's click passes host tracker → vendor redirect → landing page. The SDK
|
|
61
|
+
* does not fire that tracker itself; it would count the click twice. Without one it is empty, the
|
|
62
|
+
* documented "no player tracker" value. VAST click beacons are reported separately by
|
|
63
|
+
* `useSidebarVastTracker`.
|
|
64
|
+
*/
|
|
65
|
+
const MACRO_VALUES = {
|
|
66
|
+
// One family, one value: every name here means "make this request unique so nothing caches it".
|
|
67
|
+
// Epoch milliseconds satisfies that for all of them, and keeps the value already shipped for
|
|
68
|
+
// `timestamp` unchanged. (IAB defines `[CACHEBUSTING]` as a random 8-digit number and
|
|
69
|
+
// `[TIMESTAMP]` as ISO-8601, but those describe VAST *tracking* URLs; this substitution only
|
|
70
|
+
// ever touches the ad REQUEST url, where every ad server documents the parameter as a number.)
|
|
71
|
+
timestamp: ({
|
|
72
|
+
now
|
|
73
|
+
}) => String(now),
|
|
74
|
+
cachebuster: ({
|
|
75
|
+
now
|
|
76
|
+
}) => String(now),
|
|
77
|
+
cachebusting: ({
|
|
78
|
+
now
|
|
79
|
+
}) => String(now),
|
|
80
|
+
clickmacro: ({
|
|
81
|
+
params
|
|
82
|
+
}) => hostValue(params === null || params === void 0 ? void 0 : params.clickUrl),
|
|
83
|
+
clickurl: ({
|
|
84
|
+
params
|
|
85
|
+
}) => hostValue(params === null || params === void 0 ? void 0 : params.clickUrl),
|
|
86
|
+
// `1` / `0` only from an explicit boolean — an absent value is unknown, not "does not apply".
|
|
87
|
+
gdpr: ({
|
|
88
|
+
params
|
|
89
|
+
}) => (params === null || params === void 0 ? void 0 : params.gdprApplies) === undefined ? '' : params.gdprApplies ? '1' : '0',
|
|
90
|
+
gdpr_consent: gdprConsent
|
|
91
|
+
};
|
|
92
|
+
/** `GDPR_CONSENT_<gvlid>`: the vendor id names whose tag this is; the value is the same TC string. */
|
|
93
|
+
const GDPR_CONSENT_FOR_VENDOR = /^gdpr_consent_\d+$/;
|
|
94
|
+
const resolveMacro = name => {
|
|
95
|
+
var _MACRO_VALUES$name;
|
|
96
|
+
return (_MACRO_VALUES$name = MACRO_VALUES[name]) !== null && _MACRO_VALUES$name !== void 0 ? _MACRO_VALUES$name : GDPR_CONSENT_FOR_VENDOR.test(name) ? gdprConsent : undefined;
|
|
97
|
+
};
|
|
98
|
+
/**
|
|
99
|
+
* Fill the macros this SDK knows in an ad-tag URL. One timestamp per call, so a tag naming the macro
|
|
100
|
+
* twice stays self-consistent. `params` is read as given — the caller passes the host's CURRENT
|
|
101
|
+
* values at request time.
|
|
102
|
+
*/
|
|
103
|
+
const substituteAdTagMacros = (url, params) => {
|
|
104
|
+
const now = Date.now();
|
|
105
|
+
return url.replace(MACRO_PATTERN, (match, bracketed, braced) => {
|
|
106
|
+
var _ref;
|
|
107
|
+
const value = resolveMacro(((_ref = bracketed !== null && bracketed !== void 0 ? bracketed : braced) !== null && _ref !== void 0 ? _ref : '').toLowerCase());
|
|
108
|
+
return value ? value({
|
|
109
|
+
now,
|
|
110
|
+
params
|
|
111
|
+
}) : match;
|
|
112
|
+
});
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
exports.substituteAdTagMacros = substituteAdTagMacros;
|