• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

prebid / Prebid.js / 21359463030

26 Jan 2026 01:31PM UTC coverage: 96.218% (+0.003%) from 96.215%
21359463030

push

github

web-flow
Core: adding ima params to local cache request (#14312)

* Core: adding ima params to local cache request

* retrieving ima params

* usp data handler

41687 of 51254 branches covered (81.33%)

6 of 15 new or added lines in 1 file covered. (40.0%)

53 existing lines in 6 files now uncovered.

208851 of 217061 relevant lines covered (96.22%)

71.61 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

88.06
/modules/gamAdServerVideo.js
1
/**
1✔
2
 * This module adds [GAM support]{@link https://www.doubleclickbygoogle.com/} for Video to Prebid.
3
 */
4

5
import { getSignals } from '../libraries/gptUtils/gptUtils.js';
6
import { registerVideoSupport } from '../src/adServerManager.js';
7
import { getPPID } from '../src/adserver.js';
8
import { auctionManager } from '../src/auctionManager.js';
9
import { config } from '../src/config.js';
10
import { EVENTS } from '../src/constants.js';
11
import * as events from '../src/events.js';
12
import { getHook } from '../src/hook.js';
13
import { getRefererInfo } from '../src/refererDetection.js';
14
import { targeting } from '../src/targeting.js';
15
import {
16
  buildUrl,
17
  formatQS,
18
  isEmpty,
19
  isNumber,
20
  logError,
21
  logWarn,
22
  parseSizesInput,
23
  parseUrl
24
} from '../src/utils.js';
25
import {DEFAULT_GAM_PARAMS, GAM_ENDPOINT, gdprParams} from '../libraries/gamUtils/gamUtils.js';
26
import { vastLocalCache } from '../src/videoCache.js';
27
import { fetch } from '../src/ajax.js';
28
import XMLUtil from '../libraries/xmlUtils/xmlUtils.js';
29

30
import {getGlobalVarName} from '../src/buildOptions.js';
31
import { uspDataHandler } from '../src/consentHandler.js';
32
/**
33
 * @typedef {Object} DfpVideoParams
34
 *
35
 * This object contains the params needed to form a URL which hits the
36
 * [DFP API]{@link https://support.google.com/dfp_premium/answer/1068325?hl=en}.
37
 *
38
 * All params (except iu, mentioned below) should be considered optional. This module will choose reasonable
39
 * defaults for all of the other required params.
40
 *
41
 * The cust_params property, if present, must be an object. It will be merged with the rest of the
42
 * standard Prebid targeting params (hb_adid, hb_bidder, etc).
43
 *
44
 * @param {string} iu This param *must* be included, in order for us to create a valid request.
45
 * @param [string] description_url This field is required if you want Ad Exchange to bid on our ad unit...
46
 *   but otherwise optional
47
 */
48

49
/**
50
 * @typedef {Object} DfpVideoOptions
51
 *
52
 * @param {Object} adUnit The adUnit which this bid is supposed to help fill.
53
 * @param [Object] bid The bid which should be considered alongside the rest of the adserver's demand.
54
 *   If this isn't defined, then we'll use the winning bid for the adUnit.
55
 *
56
 * @param {DfpVideoParams} [params] Query params which should be set on the DFP request.
57
 *   These will override this module's defaults whenever they conflict.
58
 * @param {string} [url] video adserver url
59
 */
60

61
export const dep = {
1✔
62
  ri: getRefererInfo
63
}
64

65
export const VAST_TAG_URI_TAGNAME = 'VASTAdTagURI';
1✔
66

67
/**
68
 * Merge all the bid data and publisher-supplied options into a single URL, and then return it.
69
 *
70
 * @see [The DFP API]{@link https://support.google.com/dfp_premium/answer/1068325?hl=en#env} for details.
71
 *
72
 * @param {DfpVideoOptions} options Options which should be used to construct the URL.
73
 *
74
 * @return {string} A URL which calls DFP, letting options.bid
75
 *   (or the auction's winning bid for this adUnit, if undefined) compete alongside the rest of the
76
 *   demand in DFP.
77
 */
78
export function buildGamVideoUrl(options) {
79
  if (!options.params && !options.url) {
83✔
80
    logError(`A params object or a url is required to use ${getGlobalVarName()}.adServers.gam.buildVideoUrl`);
5✔
81
    return;
5✔
82
  }
83

84
  const adUnit = options.adUnit;
78✔
85
  const bid = options.bid || targeting.getWinningBids(adUnit.code)[0];
78✔
86

87
  let urlComponents = {};
78✔
88

89
  if (options.url) {
78✔
90
    // when both `url` and `params` are given, parsed url will be overwriten
91
    // with any matching param components
92
    urlComponents = parseUrl(options.url, {noDecodeWholeURL: true});
11✔
93

94
    if (isEmpty(options.params)) {
11✔
95
      return buildUrlFromAdserverUrlComponents(urlComponents, bid, options);
2✔
96
    }
97
  }
98

99
  const derivedParams = {
76✔
100
    correlator: Date.now(),
101
    sz: parseSizesInput(adUnit?.mediaTypes?.video?.playerSize).join('|'),
102
    url: encodeURIComponent(location.href),
103
  };
104

105
  const urlSearchComponent = urlComponents.search;
76✔
106
  const urlSzParam = urlSearchComponent && urlSearchComponent.sz;
76✔
107
  if (urlSzParam) {
76✔
108
    derivedParams.sz = urlSzParam + '|' + derivedParams.sz;
4✔
109
  }
110

111
  const encodedCustomParams = getCustParams(bid, options, urlSearchComponent && urlSearchComponent.cust_params);
76✔
112

113
  const queryParams = Object.assign({},
76✔
114
    DEFAULT_GAM_PARAMS,
115
    urlComponents.search,
116
    derivedParams,
117
    options.params,
118
    { cust_params: encodedCustomParams },
119
    gdprParams()
120
  );
121

122
  const descriptionUrl = getDescriptionUrl(bid, options, 'params');
76✔
123
  if (descriptionUrl) { queryParams.description_url = descriptionUrl; }
76✔
124

125
  if (!queryParams.ppid) {
76✔
126
    const ppid = getPPID();
76✔
127
    if (ppid != null) {
76✔
128
      queryParams.ppid = ppid;
2✔
129
    }
130
  }
131

132
  const video = options.adUnit?.mediaTypes?.video;
76✔
133
  Object.entries({
76✔
134
    plcmt: () => video?.plcmt,
75✔
135
    min_ad_duration: () => isNumber(video?.minduration) ? video.minduration * 1000 : null,
75✔
136
    max_ad_duration: () => isNumber(video?.maxduration) ? video.maxduration * 1000 : null,
75✔
137
    vpos() {
138
      const startdelay = video?.startdelay;
72✔
139
      if (isNumber(startdelay)) {
72✔
140
        if (startdelay === -2) return 'postroll';
4✔
141
        if (startdelay === -1 || startdelay > 0) return 'midroll';
3✔
142
        return 'preroll';
1✔
143
      }
144
    },
145
    vconp: () => Array.isArray(video?.playbackmethod) && video.playbackmethod.some(m => m === 7) ? '2' : undefined,
74✔
146
    vpa() {
147
      // playbackmethod = 3 is play on click; 1, 2, 4, 5, 6 are autoplay
148
      if (Array.isArray(video?.playbackmethod)) {
73✔
149
        const click = video.playbackmethod.some(m => m === 3);
29✔
150
        const auto = video.playbackmethod.some(m => [1, 2, 4, 5, 6].includes(m));
16✔
151
        if (click && !auto) return 'click';
13✔
152
        if (auto && !click) return 'auto';
12✔
153
      }
154
    },
155
    vpmute() {
156
      // playbackmethod = 2, 6 are muted; 1, 3, 4, 5 are not
157
      if (Array.isArray(video?.playbackmethod)) {
73✔
158
        const muted = video.playbackmethod.some(m => [2, 6].includes(m));
26✔
159
        const talkie = video.playbackmethod.some(m => [1, 3, 4, 5].includes(m));
17✔
160
        if (muted && !talkie) return '1';
13✔
161
        if (talkie && !muted) return '0';
12✔
162
      }
163
    }
164
  }).forEach(([param, getter]) => {
532✔
165
    if (!queryParams.hasOwnProperty(param)) {
532✔
166
      const val = getter();
517✔
167
      if (val != null) {
517✔
168
        queryParams[param] = val;
33✔
169
      }
170
    }
171
  });
172
  const fpd = auctionManager.index.getBidRequest(options.bid || {})?.ortb2 ??
76✔
173
    auctionManager.index.getAuction(options.bid || {})?.getFPD()?.global;
74✔
174

175
  const signals = getSignals(fpd);
76✔
176

177
  if (signals.length) {
76✔
178
    queryParams.ppsj = btoa(JSON.stringify({
4✔
179
      PublisherProvidedTaxonomySignals: signals
180
    }))
181
  }
182

183
  return buildUrl(Object.assign({}, GAM_ENDPOINT, urlComponents, { search: queryParams }));
76✔
184
}
185

186
export function notifyTranslationModule(fn) {
187
  fn.call(this, 'dfp');
×
188
}
189

190
if (config.getConfig('brandCategoryTranslation.translationFile')) { getHook('registerAdserver').before(notifyTranslationModule); }
1!
191

192
/**
193
 * Builds a video url from a base dfp video url and a winning bid, appending
194
 * Prebid-specific key-values.
195
 * @param {Object} components base video adserver url parsed into components object
196
 * @param {Object} bid winning bid object to append parameters from
197
 * @param {Object} options Options which should be used to construct the URL (used for custom params).
198
 * @return {string} video url
199
 */
200
function buildUrlFromAdserverUrlComponents(components, bid, options) {
201
  const descriptionUrl = getDescriptionUrl(bid, components, 'search');
2✔
202
  if (descriptionUrl) {
2✔
203
    components.search.description_url = descriptionUrl;
2✔
204
  }
205

206
  components.search.cust_params = getCustParams(bid, options, components.search.cust_params);
2✔
207
  return buildUrl(components);
2✔
208
}
209

210
/**
211
 * Returns the encoded vast url if it exists on a bid object, only if prebid-cache
212
 * is disabled, and description_url is not already set on a given input
213
 * @param {Object} bid object to check for vast url
214
 * @param {Object} components the object to check that description_url is NOT set on
215
 * @param {string} prop the property of components that would contain description_url
216
 * @return {string | undefined} The encoded vast url if it exists, or undefined
217
 */
218
function getDescriptionUrl(bid, components, prop) {
219
  return components?.[prop]?.description_url || encodeURIComponent(dep.ri().page);
78✔
220
}
221

222
/**
223
 * Returns the encoded `cust_params` from the bid.adserverTargeting and adds the `hb_uuid`, and `hb_cache_id`. Optionally the options.params.cust_params
224
 * @param {Object} bid
225
 * @param {Object} options this is the options passed in from the `buildGamVideoUrl` function
226
 * @return {Object} Encoded key value pairs for cust_params
227
 */
228
function getCustParams(bid, options, urlCustParams) {
229
  const adserverTargeting = (bid && bid.adserverTargeting) || {};
78✔
230

231
  let allTargetingData = {};
78✔
232
  const adUnit = options && options.adUnit;
78✔
233
  if (adUnit) {
78✔
234
    const allTargeting = targeting.getAllTargeting(adUnit.code);
78✔
235
    allTargetingData = (allTargeting) ? allTargeting[adUnit.code] : {};
78!
236
  }
237

238
  const prebidTargetingSet = Object.assign({},
78✔
239
    // Why are we adding standard keys here ? Refer https://github.com/prebid/Prebid.js/issues/3664
240
    { hb_uuid: bid && bid.videoCacheKey },
155✔
241
    // hb_cache_id became optional in prebid 5.0 after 4.x enabled the concept of optional keys. Discussion led to reversing the prior expectation of deprecating hb_uuid
242
    { hb_cache_id: bid && bid.videoCacheKey },
155✔
243
    allTargetingData,
244
    adserverTargeting,
245
  );
246

247
  // TODO: WTF is this? just firing random events, guessing at the argument, hoping noone notices?
248
  events.emit(EVENTS.SET_TARGETING, {[adUnit.code]: prebidTargetingSet});
78✔
249

250
  // merge the prebid + publisher targeting sets
251
  const publisherTargetingSet = options?.params?.cust_params;
78✔
252
  const targetingSet = Object.assign({}, prebidTargetingSet, publisherTargetingSet);
78✔
253
  let encodedParams = encodeURIComponent(formatQS(targetingSet));
78✔
254
  if (urlCustParams) {
78✔
255
    encodedParams = urlCustParams + '%26' + encodedParams;
2✔
256
  }
257

258
  return encodedParams;
78✔
259
}
260

261
async function getVastForLocallyCachedBids(gamVastWrapper, localCacheMap) {
262
  try {
4✔
263
    const xmlUtil = XMLUtil();
4✔
264
    const xmlDoc = xmlUtil.parse(gamVastWrapper);
4✔
265
    const vastAdTagUriElement = xmlDoc.querySelectorAll(VAST_TAG_URI_TAGNAME)[0];
4✔
266

267
    if (!vastAdTagUriElement || !vastAdTagUriElement.textContent) {
4!
268
      return gamVastWrapper;
×
269
    }
270

271
    const uuidExp = new RegExp(`[A-Fa-f0-9]{8}-(?:[A-Fa-f0-9]{4}-){3}[A-Fa-f0-9]{12}`, 'gi');
4✔
272
    const matchResult = Array.from(vastAdTagUriElement.textContent.matchAll(uuidExp));
4✔
273
    const uuidCandidates = matchResult
4✔
274
      .map(([uuid]) => uuid)
4✔
275
      .filter(uuid => localCacheMap.has(uuid));
4✔
276

277
    if (uuidCandidates.length !== 1) {
4✔
278
      logWarn(`Unable to determine unique uuid in ${VAST_TAG_URI_TAGNAME}`);
3✔
279
      return gamVastWrapper;
3✔
280
    }
281
    const uuid = uuidCandidates[0];
1✔
282

283
    const blobUrl = localCacheMap.get(uuid);
1✔
284
    const base64BlobContent = await getBase64BlobContent(blobUrl);
1✔
285
    const cdata = xmlDoc.createCDATASection(base64BlobContent);
1✔
286
    vastAdTagUriElement.textContent = '';
1✔
287
    vastAdTagUriElement.appendChild(cdata);
1✔
288
    return xmlUtil.serialize(xmlDoc);
1✔
289
  } catch (error) {
290
    logWarn('Unable to process xml', error);
×
291
    return gamVastWrapper;
×
292
  }
293
};
294

295
export async function getVastXml(options, localCacheMap = vastLocalCache) {
5✔
296
  let vastUrl = buildGamVideoUrl(options);
5✔
297

298
  const adUnit = options.adUnit;
5✔
299
  const video = adUnit?.mediaTypes?.video;
5✔
300
  const sdkApis = (video?.api || []).join(',');
5✔
301
  const usPrivacy = uspDataHandler.getConsentData?.();
5✔
302
  // Adding parameters required by ima
303
  if (config.getConfig('cache.useLocal') && window.google?.ima) {
5!
NEW
304
    vastUrl = new URL(vastUrl);
×
NEW
305
    const imaSdkVersion = `h.${window.google.ima.VERSION}`;
×
NEW
306
    vastUrl.searchParams.set('omid_p', `Google1/${imaSdkVersion}`);
×
NEW
307
    vastUrl.searchParams.set('sdkv', imaSdkVersion);
×
NEW
308
    if (sdkApis) {
×
NEW
309
      vastUrl.searchParams.set('sdk_apis', sdkApis);
×
310
    }
NEW
311
    if (usPrivacy) {
×
NEW
312
      vastUrl.searchParams.set('us_privacy', usPrivacy);
×
313
    }
314

NEW
315
    vastUrl = vastUrl.toString();
×
316
  }
317

318
  const response = await fetch(vastUrl);
5✔
319
  if (!response.ok) {
5!
320
    throw new Error('Unable to fetch GAM VAST wrapper');
×
321
  }
322

323
  const gamVastWrapper = await response.text();
5✔
324

325
  if (config.getConfig('cache.useLocal')) {
5✔
326
    const vastXml = await getVastForLocallyCachedBids(gamVastWrapper, localCacheMap);
4✔
327
    return vastXml;
4✔
328
  }
329

330
  return gamVastWrapper;
1✔
331
}
332

333
export async function getBase64BlobContent(blobUrl) {
334
  const response = await fetch(blobUrl);
1✔
335
  if (!response.ok) {
1!
336
    logError('Unable to fetch blob');
×
337
    throw new Error('Blob not found');
×
338
  }
339
  // Mechanism to handle cases where VAST tags are fetched
340
  // from a context where the blob resource is not accessible.
341
  // like IMA SDK iframe
342
  const blobContent = await response.text();
1✔
343
  const dataUrl = `data://text/xml;base64,${btoa(blobContent)}`;
1✔
344
  return dataUrl;
1✔
345
}
346

347
export { buildGamVideoUrl as buildDfpVideoUrl };
348

349
registerVideoSupport('gam', {
1✔
350
  buildVideoUrl: buildGamVideoUrl,
351
  getVastXml
352
});
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc