@streamlayer/feature-advertisement 1.1.1-rc.1345 → 1.1.1-rc.2.pf58ff685
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
package/lib/advertisement.js
CHANGED
|
@@ -7,6 +7,7 @@ import { SsaiCreativeCache } from './units/ssai-creative.js';
|
|
|
7
7
|
import { resolveProvider } from './units/ssai-providers.js';
|
|
8
8
|
import { createSsaiDetector, } from './units/ssai-detector.js';
|
|
9
9
|
import { createSsaiAdBreakStore } from './units/ssai-ad-break.js';
|
|
10
|
+
import { ssaiq } from './units/ssai-log.js';
|
|
10
11
|
import { preloadCreativeAssets } from './units/ssai-preload.js';
|
|
11
12
|
export class Advertisement extends AbstractFeatureUnit {
|
|
12
13
|
getExternalAd;
|
|
@@ -121,10 +122,25 @@ export class Advertisement extends AbstractFeatureUnit {
|
|
|
121
122
|
const provider = resolveProvider(options.provider);
|
|
122
123
|
const cache = new SsaiCreativeCache(provider);
|
|
123
124
|
this.ssaiCache = cache;
|
|
125
|
+
/**
|
|
126
|
+
* Every beacon the SDK fires gets a line, with the URL it was fired at.
|
|
127
|
+
* The narrative log is read as a live trace of one ad, and "we called the
|
|
128
|
+
* ad server's own tracking URL" is as much of the story as the request
|
|
129
|
+
* that fetched the creative in the first place.
|
|
130
|
+
*/
|
|
131
|
+
const logBeacon = (creativeId, event) => {
|
|
132
|
+
const url = cache.peek(creativeId)?.trackingEvents?.find((entry) => entry.event === event)?.url;
|
|
133
|
+
ssaiq.info({ creativeId, event, url }, `[beacon] ${event} fired — ${creativeId}`);
|
|
134
|
+
};
|
|
124
135
|
this.fireSsaiImpression = (creativeId) => {
|
|
136
|
+
// The UI only calls this once the bar is actually rendered, so this is
|
|
137
|
+
// the moment the viewer saw the ad — not the moment it resolved.
|
|
138
|
+
ssaiq.info({ creativeId }, `[ad] ad on screen — ${cache.peek(creativeId)?.advertiser || creativeId}`);
|
|
139
|
+
logBeacon(creativeId, 'impression');
|
|
125
140
|
cache.fireImpressionById(creativeId);
|
|
126
141
|
};
|
|
127
142
|
this.fireSsaiTracking = (creativeId, event) => {
|
|
143
|
+
logBeacon(creativeId, event);
|
|
128
144
|
cache.fireTrackingById(creativeId, event);
|
|
129
145
|
};
|
|
130
146
|
this.fireSsaiCompanionClick = (creativeId) => {
|
|
@@ -144,12 +160,14 @@ export class Advertisement extends AbstractFeatureUnit {
|
|
|
144
160
|
const prefetchCreative = (creativeId) => {
|
|
145
161
|
cache
|
|
146
162
|
.resolve(creativeId)
|
|
147
|
-
.then((creative) => preloadCreativeAssets(creative))
|
|
148
|
-
.then(() => {
|
|
163
|
+
.then((creative) => preloadCreativeAssets(creative).then(() => creative))
|
|
164
|
+
.then((creative) => {
|
|
149
165
|
preloadedIds.add(creativeId);
|
|
166
|
+
ssaiq.info({ creativeId }, `[ad] creative ready — ${creative.advertiser} (artwork preloaded)`);
|
|
150
167
|
})
|
|
151
|
-
.catch((
|
|
152
|
-
|
|
168
|
+
.catch(() => {
|
|
169
|
+
// Already on `ssaiq` from the provider; the break may still be
|
|
170
|
+
// minutes away, and `loadCreative` retries when it actually starts.
|
|
153
171
|
});
|
|
154
172
|
};
|
|
155
173
|
const publishReady = (range, creative) => {
|
|
@@ -214,11 +232,9 @@ export class Advertisement extends AbstractFeatureUnit {
|
|
|
214
232
|
videoRef: options.videoRef,
|
|
215
233
|
manifestUrl: options.manifestUrl,
|
|
216
234
|
onAdDiscovered: (creativeId) => {
|
|
217
|
-
this.log.debug({ creativeId }, 'ssai ad discovered, prefetching creative');
|
|
218
235
|
prefetchCreative(creativeId);
|
|
219
236
|
},
|
|
220
237
|
onBreakStart: (range) => {
|
|
221
|
-
this.log.debug({ creativeId: range.creativeId }, 'ssai break start');
|
|
222
238
|
eventBus.emit('ssaiAdBreak', {
|
|
223
239
|
action: 'breakStart',
|
|
224
240
|
payload: { creativeId: range.creativeId },
|
|
@@ -226,7 +242,6 @@ export class Advertisement extends AbstractFeatureUnit {
|
|
|
226
242
|
loadCreative(range);
|
|
227
243
|
},
|
|
228
244
|
onCreativeChange: (range) => {
|
|
229
|
-
this.log.debug({ creativeId: range.creativeId }, 'ssai creative change');
|
|
230
245
|
loadCreative(range);
|
|
231
246
|
},
|
|
232
247
|
onAdEnding: (range) => {
|
|
@@ -239,11 +254,11 @@ export class Advertisement extends AbstractFeatureUnit {
|
|
|
239
254
|
this.ssaiAdBreak.set({ ...current, status: 'ending' });
|
|
240
255
|
},
|
|
241
256
|
onProgress: (range, event) => {
|
|
242
|
-
|
|
257
|
+
// Logged by the detector as `progress:<event>` at the moment playback
|
|
258
|
+
// crosses the marker; nothing to add here.
|
|
243
259
|
cache.fireTrackingById(range.creativeId, event);
|
|
244
260
|
},
|
|
245
261
|
onBreakEnd: () => {
|
|
246
|
-
this.log.debug('ssai break end');
|
|
247
262
|
const current = this.ssaiAdBreak.get();
|
|
248
263
|
eventBus.emit('ssaiAdBreak', {
|
|
249
264
|
action: 'breakEnd',
|
package/lib/cjs/advertisement.js
CHANGED
|
@@ -9,6 +9,7 @@ var ssaiCreative = require('./units/ssai-creative.js');
|
|
|
9
9
|
var ssaiProviders = require('./units/ssai-providers.js');
|
|
10
10
|
var ssaiDetector = require('./units/ssai-detector.js');
|
|
11
11
|
var ssaiAdBreak = require('./units/ssai-ad-break.js');
|
|
12
|
+
var ssaiLog = require('./units/ssai-log.js');
|
|
12
13
|
var ssaiPreload = require('./units/ssai-preload.js');
|
|
13
14
|
|
|
14
15
|
class Advertisement extends sdkWebInterfaces.AbstractFeatureUnit {
|
|
@@ -124,10 +125,25 @@ class Advertisement extends sdkWebInterfaces.AbstractFeatureUnit {
|
|
|
124
125
|
const provider = ssaiProviders.resolveProvider(options.provider);
|
|
125
126
|
const cache = new ssaiCreative.SsaiCreativeCache(provider);
|
|
126
127
|
this.ssaiCache = cache;
|
|
128
|
+
/**
|
|
129
|
+
* Every beacon the SDK fires gets a line, with the URL it was fired at.
|
|
130
|
+
* The narrative log is read as a live trace of one ad, and "we called the
|
|
131
|
+
* ad server's own tracking URL" is as much of the story as the request
|
|
132
|
+
* that fetched the creative in the first place.
|
|
133
|
+
*/
|
|
134
|
+
const logBeacon = (creativeId, event) => {
|
|
135
|
+
const url = cache.peek(creativeId)?.trackingEvents?.find((entry) => entry.event === event)?.url;
|
|
136
|
+
ssaiLog.ssaiq.info({ creativeId, event, url }, `[beacon] ${event} fired — ${creativeId}`);
|
|
137
|
+
};
|
|
127
138
|
this.fireSsaiImpression = (creativeId) => {
|
|
139
|
+
// The UI only calls this once the bar is actually rendered, so this is
|
|
140
|
+
// the moment the viewer saw the ad — not the moment it resolved.
|
|
141
|
+
ssaiLog.ssaiq.info({ creativeId }, `[ad] ad on screen — ${cache.peek(creativeId)?.advertiser || creativeId}`);
|
|
142
|
+
logBeacon(creativeId, 'impression');
|
|
128
143
|
cache.fireImpressionById(creativeId);
|
|
129
144
|
};
|
|
130
145
|
this.fireSsaiTracking = (creativeId, event) => {
|
|
146
|
+
logBeacon(creativeId, event);
|
|
131
147
|
cache.fireTrackingById(creativeId, event);
|
|
132
148
|
};
|
|
133
149
|
this.fireSsaiCompanionClick = (creativeId) => {
|
|
@@ -147,12 +163,14 @@ class Advertisement extends sdkWebInterfaces.AbstractFeatureUnit {
|
|
|
147
163
|
const prefetchCreative = (creativeId) => {
|
|
148
164
|
cache
|
|
149
165
|
.resolve(creativeId)
|
|
150
|
-
.then((creative) => ssaiPreload.preloadCreativeAssets(creative))
|
|
151
|
-
.then(() => {
|
|
166
|
+
.then((creative) => ssaiPreload.preloadCreativeAssets(creative).then(() => creative))
|
|
167
|
+
.then((creative) => {
|
|
152
168
|
preloadedIds.add(creativeId);
|
|
169
|
+
ssaiLog.ssaiq.info({ creativeId }, `[ad] creative ready — ${creative.advertiser} (artwork preloaded)`);
|
|
153
170
|
})
|
|
154
|
-
.catch((
|
|
155
|
-
|
|
171
|
+
.catch(() => {
|
|
172
|
+
// Already on `ssaiq` from the provider; the break may still be
|
|
173
|
+
// minutes away, and `loadCreative` retries when it actually starts.
|
|
156
174
|
});
|
|
157
175
|
};
|
|
158
176
|
const publishReady = (range, creative) => {
|
|
@@ -217,11 +235,9 @@ class Advertisement extends sdkWebInterfaces.AbstractFeatureUnit {
|
|
|
217
235
|
videoRef: options.videoRef,
|
|
218
236
|
manifestUrl: options.manifestUrl,
|
|
219
237
|
onAdDiscovered: (creativeId) => {
|
|
220
|
-
this.log.debug({ creativeId }, 'ssai ad discovered, prefetching creative');
|
|
221
238
|
prefetchCreative(creativeId);
|
|
222
239
|
},
|
|
223
240
|
onBreakStart: (range) => {
|
|
224
|
-
this.log.debug({ creativeId: range.creativeId }, 'ssai break start');
|
|
225
241
|
sdkWebInterfaces.eventBus.emit('ssaiAdBreak', {
|
|
226
242
|
action: 'breakStart',
|
|
227
243
|
payload: { creativeId: range.creativeId },
|
|
@@ -229,7 +245,6 @@ class Advertisement extends sdkWebInterfaces.AbstractFeatureUnit {
|
|
|
229
245
|
loadCreative(range);
|
|
230
246
|
},
|
|
231
247
|
onCreativeChange: (range) => {
|
|
232
|
-
this.log.debug({ creativeId: range.creativeId }, 'ssai creative change');
|
|
233
248
|
loadCreative(range);
|
|
234
249
|
},
|
|
235
250
|
onAdEnding: (range) => {
|
|
@@ -242,11 +257,11 @@ class Advertisement extends sdkWebInterfaces.AbstractFeatureUnit {
|
|
|
242
257
|
this.ssaiAdBreak.set({ ...current, status: 'ending' });
|
|
243
258
|
},
|
|
244
259
|
onProgress: (range, event) => {
|
|
245
|
-
|
|
260
|
+
// Logged by the detector as `progress:<event>` at the moment playback
|
|
261
|
+
// crosses the marker; nothing to add here.
|
|
246
262
|
cache.fireTrackingById(range.creativeId, event);
|
|
247
263
|
},
|
|
248
264
|
onBreakEnd: () => {
|
|
249
|
-
this.log.debug('ssai break end');
|
|
250
265
|
const current = this.ssaiAdBreak.get();
|
|
251
266
|
sdkWebInterfaces.eventBus.emit('ssaiAdBreak', {
|
|
252
267
|
action: 'breakEnd',
|
package/lib/cjs/index.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
/// <reference path="../vast-client.d.ts" />
|
|
1
2
|
import { StreamLayerContext } from '@streamlayer/sdk-web-interfaces';
|
|
2
3
|
declare module '@streamlayer/sdk-web-interfaces' {
|
|
3
4
|
interface StreamLayerSDK {
|
|
@@ -9,6 +10,7 @@ export { type GAMStaticCore, type PauseAdTemplate, isFullbleedVideoTemplate } fr
|
|
|
9
10
|
export { INSIGHT_BODY_MAX_LENGTH, type PauseAdCard, type PauseAdKind, type PauseAdMediaMode, } from './units/pause-ad-card.js';
|
|
10
11
|
export { type PauseAdStore } from './units/pause-ad.js';
|
|
11
12
|
export { type AdStore, type AdData, type AdCompanion, type AdNonLinear } from './units/external-ad.js';
|
|
13
|
+
export { selectSponsorLogo, type SponsorLogoAsset } from './units/sponsor-logo.js';
|
|
12
14
|
export { gam, type GamNonceResult, type PalNonceManager } from './units/gam.js';
|
|
13
15
|
export { createCompanionTracker, createVastTracker, resolveVastTrackerTarget, type VastTrackerTarget, } from './units/vast-tracker.js';
|
|
14
16
|
export { type SsaiCreative, type SsaiTrackingEvent, type SsaiCompanion, type SsaiProgressEvent, DEFAULT_CREATIVE_API_BASE, } from './units/ssai-creative.js';
|
package/lib/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
|
var ssaiProviders = require('./units/ssai-providers.js');
|
|
@@ -21,6 +22,7 @@ const advertisement = (instance, _opts, done) => {
|
|
|
21
22
|
|
|
22
23
|
exports.isFullbleedVideoTemplate = externalPauseAd.isFullbleedVideoTemplate;
|
|
23
24
|
exports.INSIGHT_BODY_MAX_LENGTH = pauseAdCard.INSIGHT_BODY_MAX_LENGTH;
|
|
25
|
+
exports.selectSponsorLogo = sponsorLogo.selectSponsorLogo;
|
|
24
26
|
exports.gam = gam.gam;
|
|
25
27
|
exports.createCompanionTracker = vastTracker.createCompanionTracker;
|
|
26
28
|
exports.createVastTracker = vastTracker.createVastTracker;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { AdTagParams } from '@streamlayer/sdk-web-core';
|
|
2
|
+
/**
|
|
3
|
+
* Fill the macros this SDK knows in an ad-tag URL. One timestamp per call, so a tag naming the macro
|
|
4
|
+
* twice stays self-consistent. `params` is read as given — the caller passes the host's CURRENT
|
|
5
|
+
* values at request time.
|
|
6
|
+
*/
|
|
7
|
+
export declare const substituteAdTagMacros: (url: string, params?: AdTagParams) => string;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Ad-tag macro substitution, for every ad server — not just GAM.
|
|
5
|
+
*
|
|
6
|
+
* Studio stores a programmatic tag verbatim (`gamRawUrl`, returned untouched by
|
|
7
|
+
* `processGamAdvertisement`), so whatever macros the ad server's own tag builder emitted reach the
|
|
8
|
+
* SDK literal. `gam()` used to fill only the GAM `[placeholder]` token, and only for
|
|
9
|
+
* `pubads.g.doubleclick.net` — every other vendor's tag went out with its macros unexpanded.
|
|
10
|
+
*
|
|
11
|
+
* That is not inert. Measured against the ADITION tag on event 11089
|
|
12
|
+
* (`…/banner?…&ts=[timestamp]&clickurl=[clickmacro]`): ADITION prepends the value of `clickurl` to
|
|
13
|
+
* its own redi URL, so a literal `[clickmacro]` came back inside `<CompanionClickThrough>` as
|
|
14
|
+
* `[clickmacro]https%3A%2F%2Fad1…` — a broken click-through. Sending `clickurl=` empty answers with
|
|
15
|
+
* a valid redi URL.
|
|
16
|
+
*
|
|
17
|
+
* **Two sources.** The cache buster is the SDK's own. The consent family — `${GDPR}` and
|
|
18
|
+
* `${GDPR_CONSENT_<gvlid>}`, IAB TCF, where the id is the vendor's Global Vendor List entry (39 is
|
|
19
|
+
* ADITION / Virtual Minds) — and the click macro come from the host app (`AdTagParams`, set through
|
|
20
|
+
* the `adTagParams` option or `sdk.setAdTagParams`). The SDK never reads a CMP and never guesses
|
|
21
|
+
* consent: a value the host did not pass goes out EMPTY, which ad servers read as "unknown" — never
|
|
22
|
+
* as `gdpr=0`. Empty rather than literal because a literal is not inert either: ADITION copied
|
|
23
|
+
* `${GDPR}` into its own redirect URL (`gdpr%3D%24%7BGDPR%7D`). See
|
|
24
|
+
* `docs/wiki/programmatic-ads-macros.md`.
|
|
25
|
+
*
|
|
26
|
+
* An unknown macro is left exactly as configured, which is what keeps the GAM `[placeholder]`
|
|
27
|
+
* tokens for the PAL path (`gam.ts` fills those per parameter, not per token).
|
|
28
|
+
*/
|
|
29
|
+
/**
|
|
30
|
+
* All three spellings in the wild — `[NAME]` (IAB / SpotX), `${NAME}` (Google) and bare `{NAME}` —
|
|
31
|
+
* each with its delimiters either literal or PERCENT-ENCODED.
|
|
32
|
+
*
|
|
33
|
+
* Neither half of that is theoretical. ADITION emitted `[timestamp]` / `[clickmacro]` for the
|
|
34
|
+
* campaign on event 11089 and `{CACHEBUSTER}` / `{GDPR}` for the one on 6203 — same ad server, same
|
|
35
|
+
* advertiser, different tag builder. And Studio stores the 6203 tag as `ts=%7BCACHEBUSTER%7D`, so
|
|
36
|
+
* matching only literal braces made the whole substitution a silent no-op on it: an unexpanded
|
|
37
|
+
* macro raises no error and the ad server still answers 200. `searchParams.get()` decodes on read,
|
|
38
|
+
* which is what hides the encoding from anyone inspecting the parsed URL.
|
|
39
|
+
*
|
|
40
|
+
* The `$` is optional rather than a fourth branch — an unowned `${NAME}` still returns its full
|
|
41
|
+
* matched text, `$` included, so the brace forms cannot corrupt each other.
|
|
42
|
+
*/
|
|
43
|
+
const MACRO_PATTERN = /(?:\[|%5B)([a-z0-9_]+)(?:\]|%5D)|(?:\$|%24)?(?:\{|%7B)([a-z0-9_]+)(?:\}|%7D)/gi;
|
|
44
|
+
/**
|
|
45
|
+
* Every host value lands inside a query-string VALUE, so it is encoded exactly once. That matters
|
|
46
|
+
* most for the click URL, which carries its own `?`/`&`: sent raw it re-opens the tag's query, the
|
|
47
|
+
* way the malformed 6203 tag did (`clickurl` truncated, every following parameter duplicated).
|
|
48
|
+
*/
|
|
49
|
+
const hostValue = (value) => (value ? encodeURIComponent(value) : '');
|
|
50
|
+
const gdprConsent = ({ params }) => hostValue(params?.tcString);
|
|
51
|
+
/**
|
|
52
|
+
* Macro name (lower-cased) → the value this SDK sends for it.
|
|
53
|
+
*
|
|
54
|
+
* `clickurl` / `clickmacro` take the host's click tracker, which the ad server PREPENDS to its own
|
|
55
|
+
* redirect — so the viewer's click passes host tracker → vendor redirect → landing page. The SDK
|
|
56
|
+
* does not fire that tracker itself; it would count the click twice. Without one it is empty, the
|
|
57
|
+
* documented "no player tracker" value. VAST click beacons are reported separately by
|
|
58
|
+
* `useSidebarVastTracker`.
|
|
59
|
+
*/
|
|
60
|
+
const MACRO_VALUES = {
|
|
61
|
+
// One family, one value: every name here means "make this request unique so nothing caches it".
|
|
62
|
+
// Epoch milliseconds satisfies that for all of them, and keeps the value already shipped for
|
|
63
|
+
// `timestamp` unchanged. (IAB defines `[CACHEBUSTING]` as a random 8-digit number and
|
|
64
|
+
// `[TIMESTAMP]` as ISO-8601, but those describe VAST *tracking* URLs; this substitution only
|
|
65
|
+
// ever touches the ad REQUEST url, where every ad server documents the parameter as a number.)
|
|
66
|
+
timestamp: ({ now }) => String(now),
|
|
67
|
+
cachebuster: ({ now }) => String(now),
|
|
68
|
+
cachebusting: ({ now }) => String(now),
|
|
69
|
+
clickmacro: ({ params }) => hostValue(params?.clickUrl),
|
|
70
|
+
clickurl: ({ params }) => hostValue(params?.clickUrl),
|
|
71
|
+
// `1` / `0` only from an explicit boolean — an absent value is unknown, not "does not apply".
|
|
72
|
+
gdpr: ({ params }) => (params?.gdprApplies === undefined ? '' : params.gdprApplies ? '1' : '0'),
|
|
73
|
+
gdpr_consent: gdprConsent,
|
|
74
|
+
};
|
|
75
|
+
/** `GDPR_CONSENT_<gvlid>`: the vendor id names whose tag this is; the value is the same TC string. */
|
|
76
|
+
const GDPR_CONSENT_FOR_VENDOR = /^gdpr_consent_\d+$/;
|
|
77
|
+
const resolveMacro = (name) => MACRO_VALUES[name] ?? (GDPR_CONSENT_FOR_VENDOR.test(name) ? gdprConsent : undefined);
|
|
78
|
+
/**
|
|
79
|
+
* Fill the macros this SDK knows in an ad-tag URL. One timestamp per call, so a tag naming the macro
|
|
80
|
+
* twice stays self-consistent. `params` is read as given — the caller passes the host's CURRENT
|
|
81
|
+
* values at request time.
|
|
82
|
+
*/
|
|
83
|
+
const substituteAdTagMacros = (url, params) => {
|
|
84
|
+
const now = Date.now();
|
|
85
|
+
return url.replace(MACRO_PATTERN, (match, bracketed, braced) => {
|
|
86
|
+
const value = resolveMacro((bracketed ?? braced ?? '').toLowerCase());
|
|
87
|
+
return value ? value({ now, params }) : match;
|
|
88
|
+
});
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
exports.substituteAdTagMacros = substituteAdTagMacros;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { AdTagParams } from '@streamlayer/sdk-web-core';
|
|
1
2
|
import { StreamLayerContext } from '@streamlayer/sdk-web-interfaces';
|
|
2
3
|
import { VastAd, VastCompanionAd, VastCreativeCompanion, VastCreativeLinear, VastCreativeNonLinear, VastNonLinearAd, VastResponse } from '@dailymotion/vast-client';
|
|
3
4
|
import '@streamlayer/sdk-web-api';
|
|
@@ -35,6 +36,23 @@ export type AdData = {
|
|
|
35
36
|
companion?: AdCompanion;
|
|
36
37
|
/** The 300x75 `<Companion>` — the sponsor logo. Only set for a companion pair. */
|
|
37
38
|
logo?: AdCompanion;
|
|
39
|
+
/**
|
|
40
|
+
* The iOS `SLRVASTSlotRole.cardLogo` pick: companion-only, aspect-scored toward 1:1, and minus
|
|
41
|
+
* whatever the hero slot already claimed. Consumed **only** by the hero-card placements
|
|
42
|
+
* (Side-by-Side / Picture in Picture / Sponsor Frame) as a fallback for an otherwise empty
|
|
43
|
+
* header logo. `logo` is the SC-335 display rule and is unchanged; when it fires, this stays
|
|
44
|
+
* `undefined` because the display rule already won the slot.
|
|
45
|
+
*/
|
|
46
|
+
cardLogo?: AdCompanion;
|
|
47
|
+
/**
|
|
48
|
+
* The tag's ONLY `<Companion>`, when it is exactly 300x75 and the hero is a Linear or a
|
|
49
|
+
* NonLinear — i.e. the companion is logo-sized and nothing else needs it. The pair rule behind
|
|
50
|
+
* `logo` exists to tell the logo from the media among companions; with the media coming from
|
|
51
|
+
* elsewhere there is nothing to tell apart, and iOS fills its logo from a single companion.
|
|
52
|
+
* Consumed only by the Standard Ad Sidebar (`createActiveAdFromVast`) as a header-logo fallback;
|
|
53
|
+
* a companion-only tag leaves it `undefined`, because there the companion IS the media.
|
|
54
|
+
*/
|
|
55
|
+
loneLogo?: AdCompanion;
|
|
38
56
|
/** Banner-sized `<Companion>` (468x60 class) — the sidebar media for companion-pair tags. */
|
|
39
57
|
mediaCompanion?: AdCompanion;
|
|
40
58
|
/** NonLinear (image-only) creative — present when the tag has no `<Linear>` media. */
|
|
@@ -55,4 +73,55 @@ export type AdStore = {
|
|
|
55
73
|
muted: boolean;
|
|
56
74
|
nonceManager?: PalNonceManager;
|
|
57
75
|
};
|
|
76
|
+
/**
|
|
77
|
+
* Split a `<CompanionAds>` creative's variations into the logo and media slots.
|
|
78
|
+
*
|
|
79
|
+
* The pair mapping activates only for a tag that ships an exact 300x75 companion next to at
|
|
80
|
+
* least one other. Deliberately conservative pending broader product sizes: on any other tag
|
|
81
|
+
* no companion is promoted to the logo slot and the first one stays the media/strip candidate.
|
|
82
|
+
*
|
|
83
|
+
* Both halves of that gate are load-bearing and neither is an aspect rule: a 320x80 companion is
|
|
84
|
+
* the same 4:1 shape as 300x75 and is still rejected, and a lone 300x75 companion is rejected too
|
|
85
|
+
* (the Standard Ad Sidebar recovers that one as `AdData.loneLogo` when a Linear / NonLinear is
|
|
86
|
+
* the media, so it is not needed here).
|
|
87
|
+
* This is **narrower than iOS**, which has no size rule at all and fills its logo slot from a
|
|
88
|
+
* single companion — see `docs/wiki/programmatic-ads-display-ios-parity.md`. Exported for
|
|
89
|
+
* `external-ad.spec.ts` only; `src/index.ts` re-exports names explicitly, so this stays out of the
|
|
90
|
+
* package's public API.
|
|
91
|
+
*/
|
|
92
|
+
export declare const selectCompanions: (companions: AdCompanion[]) => {
|
|
93
|
+
logo: undefined;
|
|
94
|
+
media: AdCompanion;
|
|
95
|
+
} | {
|
|
96
|
+
logo: AdCompanion;
|
|
97
|
+
media: AdCompanion | undefined;
|
|
98
|
+
};
|
|
99
|
+
/**
|
|
100
|
+
* Map a parsed VAST response into our `AdData` shape. Shared by both acquisition
|
|
101
|
+
* paths — the GAM/VMAP tag flow and the Prebid auction flow — so the two always
|
|
102
|
+
* produce identical downstream data.
|
|
103
|
+
*
|
|
104
|
+
* Returns `undefined` for a no-ad, which is two distinct shapes: a response with no ad carrying
|
|
105
|
+
* creatives, and an ad whose creatives carry no RENDERABLE asset — no linear `<MediaFile>`, no
|
|
106
|
+
* `<NonLinear>` still and no `<Companion>` image. Both callers `filter(Boolean)` the result, so a
|
|
107
|
+
* no-ad leaves `ads: []` and the card keeps whatever the server configured for it.
|
|
108
|
+
*
|
|
109
|
+
* Exported for `vast-mapping.spec.ts`, which drives it over the same fixture corpus the iOS suite
|
|
110
|
+
* uses. `src/index.ts` re-exports names explicitly, so this stays out of the package's public API.
|
|
111
|
+
*/
|
|
112
|
+
export declare const buildAdData: (parsedVAST: VastResponse, order: number) => AdData | undefined;
|
|
113
|
+
/**
|
|
114
|
+
* Build the OpenRTB request body for a single video auction. Placement config is
|
|
115
|
+
* carried on the Prebid link's query string so the consumer-facing interface stays a
|
|
116
|
+
* single URL string:
|
|
117
|
+
* - `configId` → `imp.ext.prebid.storedrequest.id` (production stored request).
|
|
118
|
+
* - `storedauctionresponse` → `imp.ext.prebid.storedauctionresponse.id` (staging mock).
|
|
119
|
+
* - `test=1` → top-level `test` flag.
|
|
120
|
+
* - `w` / `h` → video dimensions (default 1920×1080).
|
|
121
|
+
*
|
|
122
|
+
* The host's GDPR consent (`AdTagParams`) rides the OpenRTB 2.5 extension fields Prebid Server
|
|
123
|
+
* reads — `regs.ext.gdpr` and `user.ext.consent` — the same signal the `${GDPR}` /
|
|
124
|
+
* `${GDPR_CONSENT}` macros carry for a VAST tag. Each is sent only when the host passed it.
|
|
125
|
+
*/
|
|
126
|
+
export declare const buildAuctionRequest: (url: URL, adTagParams?: AdTagParams) => Record<string, unknown>;
|
|
58
127
|
export declare const externalAd: (instance: StreamLayerContext) => (gamUrl: string) => import("@nanostores/query", { with: { "resolution-mode": "import" } }).FetcherStore<AdStore | undefined, any>;
|
|
@@ -6,6 +6,7 @@ require('@streamlayer/sdk-web-api');
|
|
|
6
6
|
var VMAP = require('vmap');
|
|
7
7
|
var gam = require('./gam.js');
|
|
8
8
|
var parseVastExtensions = require('./parse-vast-extensions.js');
|
|
9
|
+
var sponsorLogo = require('./sponsor-logo.js');
|
|
9
10
|
|
|
10
11
|
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
11
12
|
|
|
@@ -45,7 +46,7 @@ const withTimeout = (promise, ms) => {
|
|
|
45
46
|
* Get the first valid creative from an ad
|
|
46
47
|
*/
|
|
47
48
|
const getCreative = (ad) => {
|
|
48
|
-
return ad.creatives.find((creative) => creative
|
|
49
|
+
return ad.creatives.find((creative) => getMediaFileUrl(creative));
|
|
49
50
|
};
|
|
50
51
|
/**
|
|
51
52
|
* Get the URL of the first media file in the creative
|
|
@@ -66,31 +67,10 @@ const getClickThrough = (creative) => {
|
|
|
66
67
|
// @ts-ignore — vast-client types `videoClickThroughURLTemplate` loosely
|
|
67
68
|
return creative?.videoClickThroughURLTemplate?.url || undefined;
|
|
68
69
|
};
|
|
69
|
-
/**
|
|
70
|
-
* Extract every `<Companion>` variation of the first `<CompanionAds>` creative that carries a
|
|
71
|
-
* `<StaticResource>`, in document order. `width`/`height` are XML attributes (strings at
|
|
72
|
-
* runtime), normalised to numbers so the size mapping can compare them.
|
|
73
|
-
*/
|
|
74
|
-
const getCompanions = (ad) => {
|
|
75
|
-
const creative = ad.creatives.find((item) => item.type === 'companion');
|
|
76
|
-
return (creative?.variations || []).reduce((acc, variation) => {
|
|
77
|
-
const imageUrl = variation?.staticResources?.[0]?.url;
|
|
78
|
-
if (!imageUrl) {
|
|
79
|
-
return acc;
|
|
80
|
-
}
|
|
81
|
-
return acc.concat({
|
|
82
|
-
imageUrl,
|
|
83
|
-
clickUrl: variation.companionClickThroughURLTemplate ?? undefined,
|
|
84
|
-
width: Number(variation.width) || undefined,
|
|
85
|
-
height: Number(variation.height) || undefined,
|
|
86
|
-
creative,
|
|
87
|
-
variation,
|
|
88
|
-
});
|
|
89
|
-
}, []);
|
|
90
|
-
};
|
|
91
70
|
/** Product companion mapping: the 300x75 companion is the logo, the banner-sized one the media. */
|
|
92
71
|
const LOGO_COMPANION_WIDTH = 300;
|
|
93
72
|
const LOGO_COMPANION_HEIGHT = 75;
|
|
73
|
+
const isLogoSized = (companion) => companion?.width === LOGO_COMPANION_WIDTH && companion?.height === LOGO_COMPANION_HEIGHT;
|
|
94
74
|
const companionArea = (companion) => (companion.width || 0) * (companion.height || 0);
|
|
95
75
|
/**
|
|
96
76
|
* Split a `<CompanionAds>` creative's variations into the logo and media slots.
|
|
@@ -98,11 +78,18 @@ const companionArea = (companion) => (companion.width || 0) * (companion.height
|
|
|
98
78
|
* The pair mapping activates only for a tag that ships an exact 300x75 companion next to at
|
|
99
79
|
* least one other. Deliberately conservative pending broader product sizes: on any other tag
|
|
100
80
|
* no companion is promoted to the logo slot and the first one stays the media/strip candidate.
|
|
81
|
+
*
|
|
82
|
+
* Both halves of that gate are load-bearing and neither is an aspect rule: a 320x80 companion is
|
|
83
|
+
* the same 4:1 shape as 300x75 and is still rejected, and a lone 300x75 companion is rejected too
|
|
84
|
+
* (the Standard Ad Sidebar recovers that one as `AdData.loneLogo` when a Linear / NonLinear is
|
|
85
|
+
* the media, so it is not needed here).
|
|
86
|
+
* This is **narrower than iOS**, which has no size rule at all and fills its logo slot from a
|
|
87
|
+
* single companion — see `docs/wiki/programmatic-ads-display-ios-parity.md`. Exported for
|
|
88
|
+
* `external-ad.spec.ts` only; `src/index.ts` re-exports names explicitly, so this stays out of the
|
|
89
|
+
* package's public API.
|
|
101
90
|
*/
|
|
102
91
|
const selectCompanions = (companions) => {
|
|
103
|
-
const logo = companions.length > 1
|
|
104
|
-
? companions.find((companion) => companion.width === LOGO_COMPANION_WIDTH && companion.height === LOGO_COMPANION_HEIGHT)
|
|
105
|
-
: undefined;
|
|
92
|
+
const logo = companions.length > 1 ? companions.find((companion) => isLogoSized(companion)) : undefined;
|
|
106
93
|
if (!logo) {
|
|
107
94
|
return { logo: undefined, media: companions[0] };
|
|
108
95
|
}
|
|
@@ -112,40 +99,26 @@ const selectCompanions = (companions) => {
|
|
|
112
99
|
.pop();
|
|
113
100
|
return { logo, media };
|
|
114
101
|
};
|
|
115
|
-
/**
|
|
116
|
-
* Extract the first `<NonLinearAds>` creative's static-image variation. Both the creative (which
|
|
117
|
-
* owns the `<TrackingEvents>`) and the variation (which owns the click-through) are kept —
|
|
118
|
-
* `VASTTracker` needs both.
|
|
119
|
-
*
|
|
120
|
-
* `@dailymotion/vast-client` 6.4.2 emits `nonlinearClickThroughURLTemplate` (lower-case `l`),
|
|
121
|
-
* which the `@types/vast-client` shim declares as `nonLinearClickThroughURLTemplate` — the
|
|
122
|
-
* runtime spelling is authoritative.
|
|
123
|
-
*/
|
|
124
|
-
const getNonLinear = (ad) => {
|
|
125
|
-
const creative = ad.creatives.find((item) => item.type === 'nonlinear');
|
|
126
|
-
const variation = creative?.variations?.[0];
|
|
127
|
-
const imageUrl = variation?.staticResource;
|
|
128
|
-
if (!creative || !variation || !imageUrl) {
|
|
129
|
-
return undefined;
|
|
130
|
-
}
|
|
131
|
-
return {
|
|
132
|
-
imageUrl,
|
|
133
|
-
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
134
|
-
// @ts-ignore — see the runtime-vs-shim spelling note above
|
|
135
|
-
clickUrl: variation.nonlinearClickThroughURLTemplate ?? undefined,
|
|
136
|
-
width: Number(variation.width) || undefined,
|
|
137
|
-
height: Number(variation.height) || undefined,
|
|
138
|
-
creative,
|
|
139
|
-
variation,
|
|
140
|
-
};
|
|
141
|
-
};
|
|
142
102
|
/**
|
|
143
103
|
* Map a parsed VAST response into our `AdData` shape. Shared by both acquisition
|
|
144
104
|
* paths — the GAM/VMAP tag flow and the Prebid auction flow — so the two always
|
|
145
|
-
* produce identical downstream data.
|
|
146
|
-
*
|
|
105
|
+
* produce identical downstream data.
|
|
106
|
+
*
|
|
107
|
+
* Returns `undefined` for a no-ad, which is two distinct shapes: a response with no ad carrying
|
|
108
|
+
* creatives, and an ad whose creatives carry no RENDERABLE asset — no linear `<MediaFile>`, no
|
|
109
|
+
* `<NonLinear>` still and no `<Companion>` image. Both callers `filter(Boolean)` the result, so a
|
|
110
|
+
* no-ad leaves `ads: []` and the card keeps whatever the server configured for it.
|
|
111
|
+
*
|
|
112
|
+
* Exported for `vast-mapping.spec.ts`, which drives it over the same fixture corpus the iOS suite
|
|
113
|
+
* uses. `src/index.ts` re-exports names explicitly, so this stays out of the package's public API.
|
|
147
114
|
*/
|
|
148
115
|
const buildAdData = (parsedVAST, order) => {
|
|
116
|
+
// Defensive, and unreachable through either production path: `@dailymotion/vast-client` discards
|
|
117
|
+
// an ad whose creatives all fail to parse (empty `<Linear>`, empty/missing `<Creatives>`, unknown
|
|
118
|
+
// creative element) before `parseVAST` resolves, so it never yields an Ad with zero creatives.
|
|
119
|
+
// Kept because it costs nothing and `buildAdData` is also reachable from tests with a hand-built
|
|
120
|
+
// response — see the `invalid_ad` case in `vast-mapping.spec.ts`, which pins the parser contract
|
|
121
|
+
// this relies on rather than pretending to exercise the guard.
|
|
149
122
|
const validAd = parsedVAST.ads.find((ads) => ads.creatives.length > 0);
|
|
150
123
|
if (!validAd) {
|
|
151
124
|
log.warn({ order, ads: parsedVAST.ads.length }, 'vast response carries no ad with creatives — treated as no-ad');
|
|
@@ -157,10 +130,47 @@ const buildAdData = (parsedVAST, order) => {
|
|
|
157
130
|
const parsedExtension = parseVastExtensions.parseVastExtension(extension);
|
|
158
131
|
const creative = getCreative(validAd);
|
|
159
132
|
const mediaFileUrl = getMediaFileUrl(creative);
|
|
160
|
-
const
|
|
133
|
+
const images = sponsorLogo.collectAssets(validAd);
|
|
134
|
+
const companions = images.filter((asset) => asset.origin === 'companion');
|
|
135
|
+
const nonLinear = images.find((asset) => asset.origin === 'nonlinear');
|
|
136
|
+
// An ad the parser KEPT whose creatives carry nothing any shell can draw is a no-ad, exactly
|
|
137
|
+
// like a response with no ad at all. vast-client only discards a creative it cannot parse; a
|
|
138
|
+
// `<NonLinear>` or `<Companion>` whose `<StaticResource>` body is empty parses fine and just
|
|
139
|
+
// yields no url, so the guard above cannot catch it. Without this the fabricated promotion still
|
|
140
|
+
// took the slot and drew the ad's TEXT — title and description over an empty media box — which
|
|
141
|
+
// is a billable impression for a creative that does not exist.
|
|
142
|
+
//
|
|
143
|
+
// "Renderable" is the union of the three slots the shells can fill: a linear `<MediaFile>`, a
|
|
144
|
+
// `<NonLinear>` still, or a `<Companion>` image. `collectAssets` already drops the imageless
|
|
145
|
+
// ones, so a non-empty `companions` means at least one drawable image. A tag carrying only a
|
|
146
|
+
// `<Mezzanine>` never reaches here — the parser drops that Linear itself — but it lands in the
|
|
147
|
+
// same place either way.
|
|
148
|
+
if (!mediaFileUrl && !nonLinear && companions.length === 0) {
|
|
149
|
+
log.warn({ order, adId: validAd.id, creatives: validAd.creatives.length }, 'vast ad carries no renderable asset (no media file, no nonlinear, no companion) — treated as no-ad');
|
|
150
|
+
return undefined;
|
|
151
|
+
}
|
|
161
152
|
const { logo, media: mediaCompanion } = selectCompanions(companions);
|
|
162
|
-
const nonLinear = getNonLinear(validAd);
|
|
163
153
|
const clickThrough = getClickThrough(creative) || nonLinear?.clickUrl || mediaCompanion?.clickUrl;
|
|
154
|
+
// Mirror iOS's greedy `pool.select([hero, .cardLogo])`: the hero claims first and a claimed
|
|
155
|
+
// asset is gone for later roles. Web's hero precedence (`createActiveAdFromVast`) is linear
|
|
156
|
+
// video → pair media companion (unreachable here: a `logo` short-circuits the whole selection)
|
|
157
|
+
// → NonLinear still → lone companion, so a companion is only unavailable to the logo slot when
|
|
158
|
+
// it IS the hero, i.e. when the tag ships neither. `selectCardLogo` already filters to companion
|
|
159
|
+
// origins, so the NonLinear never needs excluding.
|
|
160
|
+
//
|
|
161
|
+
// Excluding `companions[0]` (which is what `mediaCompanion` is when there is no `logo`) rather
|
|
162
|
+
// than the largest-area asset is a deliberate difference from iOS's `areaDominant` hero: web's
|
|
163
|
+
// content slot draws the FIRST companion in document order, so document order is the correct
|
|
164
|
+
// criterion for "what the hero already claimed" on this platform.
|
|
165
|
+
const companionIsHero = !mediaFileUrl && !nonLinear;
|
|
166
|
+
const cardLogoAsset = logo ? undefined : sponsorLogo.selectCardLogo(validAd, companionIsHero ? [mediaCompanion] : []);
|
|
167
|
+
// Share the selected resource wrapper so drawn-companion identity remains stable.
|
|
168
|
+
const cardLogo = cardLogoAsset
|
|
169
|
+
? companions.find((item) => item.variation === cardLogoAsset.variation && item.imageUrl === cardLogoAsset.imageUrl)
|
|
170
|
+
: undefined;
|
|
171
|
+
// Same collected instance as `companion` (identity matters to `drawnCompanions`). A lone
|
|
172
|
+
// companion never satisfies the pair rule, so this can only fill a slot `logo` left empty.
|
|
173
|
+
const loneLogo = companions.length === 1 && !companionIsHero && isLogoSized(companions[0]) ? companions[0] : undefined;
|
|
164
174
|
log.info({
|
|
165
175
|
order,
|
|
166
176
|
adId: validAd.id,
|
|
@@ -176,6 +186,8 @@ const buildAdData = (parsedVAST, order) => {
|
|
|
176
186
|
media: mediaCompanion
|
|
177
187
|
? { size: `${mediaCompanion.width}x${mediaCompanion.height}`, url: mediaCompanion.imageUrl }
|
|
178
188
|
: undefined,
|
|
189
|
+
cardLogo: cardLogo ? { size: `${cardLogo.width}x${cardLogo.height}`, url: cardLogo.imageUrl } : undefined,
|
|
190
|
+
loneLogo: loneLogo ? { size: `${loneLogo.width}x${loneLogo.height}`, url: loneLogo.imageUrl } : undefined,
|
|
179
191
|
clickThrough: !!clickThrough,
|
|
180
192
|
},
|
|
181
193
|
}, 'vast ad parsed');
|
|
@@ -185,6 +197,8 @@ const buildAdData = (parsedVAST, order) => {
|
|
|
185
197
|
mediaFileUrl,
|
|
186
198
|
companion: companions[0],
|
|
187
199
|
logo,
|
|
200
|
+
cardLogo,
|
|
201
|
+
loneLogo,
|
|
188
202
|
mediaCompanion,
|
|
189
203
|
nonLinear,
|
|
190
204
|
clickThrough,
|
|
@@ -213,8 +227,12 @@ const createRequestId = () => `sl-web-${Date.now().toString(36)}-${Math.random()
|
|
|
213
227
|
* - `storedauctionresponse` → `imp.ext.prebid.storedauctionresponse.id` (staging mock).
|
|
214
228
|
* - `test=1` → top-level `test` flag.
|
|
215
229
|
* - `w` / `h` → video dimensions (default 1920×1080).
|
|
230
|
+
*
|
|
231
|
+
* The host's GDPR consent (`AdTagParams`) rides the OpenRTB 2.5 extension fields Prebid Server
|
|
232
|
+
* reads — `regs.ext.gdpr` and `user.ext.consent` — the same signal the `${GDPR}` /
|
|
233
|
+
* `${GDPR_CONSENT}` macros carry for a VAST tag. Each is sent only when the host passed it.
|
|
216
234
|
*/
|
|
217
|
-
const buildAuctionRequest = (url) => {
|
|
235
|
+
const buildAuctionRequest = (url, adTagParams) => {
|
|
218
236
|
const params = url.searchParams;
|
|
219
237
|
const width = Number(params.get('w')) || 1920;
|
|
220
238
|
const height = Number(params.get('h')) || 1080;
|
|
@@ -252,6 +270,12 @@ const buildAuctionRequest = (url) => {
|
|
|
252
270
|
if (params.get('test') === '1') {
|
|
253
271
|
request.test = 1;
|
|
254
272
|
}
|
|
273
|
+
if (adTagParams?.gdprApplies !== undefined) {
|
|
274
|
+
request.regs = { ext: { gdpr: adTagParams.gdprApplies ? 1 : 0 } };
|
|
275
|
+
}
|
|
276
|
+
if (adTagParams?.tcString) {
|
|
277
|
+
request.user = { ext: { consent: adTagParams.tcString } };
|
|
278
|
+
}
|
|
255
279
|
return request;
|
|
256
280
|
};
|
|
257
281
|
/**
|
|
@@ -259,11 +283,11 @@ const buildAuctionRequest = (url) => {
|
|
|
259
283
|
* Every bid's `adm` is inline VAST XML (no URL to fetch); bids are ordered by price
|
|
260
284
|
* so the winner plays first (RFC §2). A single bad bid must not drop the batch.
|
|
261
285
|
*/
|
|
262
|
-
const fetchPrebidAds = async (prebidUrl) => {
|
|
286
|
+
const fetchPrebidAds = async (prebidUrl, adTagParams) => {
|
|
263
287
|
const response = await fetch(prebidUrl.toString(), {
|
|
264
288
|
method: 'POST',
|
|
265
289
|
headers: { 'Content-Type': 'application/json' },
|
|
266
|
-
body: JSON.stringify(buildAuctionRequest(prebidUrl)),
|
|
290
|
+
body: JSON.stringify(buildAuctionRequest(prebidUrl, adTagParams)),
|
|
267
291
|
});
|
|
268
292
|
if (!response.ok) {
|
|
269
293
|
throw new Error(`prebid auction failed with status ${response.status}`);
|
|
@@ -316,13 +340,17 @@ const externalAd = (instance) => {
|
|
|
316
340
|
return undefined;
|
|
317
341
|
}
|
|
318
342
|
})();
|
|
343
|
+
// The host's CURRENT macro values, read per request rather than baked into the store: the
|
|
344
|
+
// store is cached by url for the session, and consent routinely arrives after the SDK
|
|
345
|
+
// starts (a CMP answers asynchronously) or changes mid-session.
|
|
346
|
+
const adTagParams = instance.sdk.options.get().adTagParams;
|
|
319
347
|
// Prebid links hit our Prebid Server auction endpoint instead of a VAST tag.
|
|
320
348
|
// Run the OpenRTB auction and parse the winning bid's inline VAST (`adm`) into
|
|
321
349
|
// the same `AdStore` shape the GAM/VMAP path returns. See rfcs/prebid.
|
|
322
350
|
if (parsedAdUrl && isPrebidUrl(parsedAdUrl)) {
|
|
323
|
-
return fetchPrebidAds(parsedAdUrl);
|
|
351
|
+
return fetchPrebidAds(parsedAdUrl, adTagParams);
|
|
324
352
|
}
|
|
325
|
-
const generateUrl = gam.gam(adUrl);
|
|
353
|
+
const generateUrl = gam.gam(adUrl, undefined, adTagParams);
|
|
326
354
|
const generatedUrl = await generateUrl();
|
|
327
355
|
const noncedLink = generatedUrl?.url;
|
|
328
356
|
if (!noncedLink) {
|
|
@@ -387,4 +415,7 @@ const externalAd = (instance) => {
|
|
|
387
415
|
});
|
|
388
416
|
};
|
|
389
417
|
|
|
418
|
+
exports.buildAdData = buildAdData;
|
|
419
|
+
exports.buildAuctionRequest = buildAuctionRequest;
|
|
390
420
|
exports.externalAd = externalAd;
|
|
421
|
+
exports.selectCompanions = selectCompanions;
|