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

prebid / Prebid.js / 16598347968

29 Jul 2025 02:01PM UTC coverage: 96.259% (+0.002%) from 96.257%
16598347968

push

github

web-flow
programmaticXBidAdapter: fix tests (#13688)

* programmaticXBidAdapter: fix tests

* Update adplusAnalyticsAdapter_spec.js

---------

Co-authored-by: Patrick McCann <patmmccann@gmail.com>

39401 of 48422 branches covered (81.37%)

51 of 51 new or added lines in 2 files covered. (100.0%)

219 existing lines in 37 files now uncovered.

194969 of 202547 relevant lines covered (96.26%)

83.47 hits per line

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

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

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

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

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

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

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

86
  let urlComponents = {};
78✔
87

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

237
  const prebidTargetingSet = Object.assign({},
78✔
238
    // Why are we adding standard keys here ? Refer https://github.com/prebid/Prebid.js/issues/3664
239
    { hb_uuid: bid && bid.videoCacheKey },
155✔
240
    // 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
241
    { hb_cache_id: bid && bid.videoCacheKey },
155✔
242
    allTargetingData,
243
    adserverTargeting,
244
  );
245

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

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

257
  return encodedParams;
78✔
258
}
259

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

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

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

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

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

294
export async function getVastXml(options, localCacheMap = vastLocalCache) {
5✔
295
  const vastUrl = buildGamVideoUrl(options);
5✔
296
  const response = await fetch(vastUrl);
5✔
297
  if (!response.ok) {
5!
UNCOV
298
    throw new Error('Unable to fetch GAM VAST wrapper');
×
299
  }
300

301
  const gamVastWrapper = await response.text();
5✔
302

303
  if (config.getConfig('cache.useLocal')) {
5✔
304
    const vastXml = await getVastForLocallyCachedBids(gamVastWrapper, localCacheMap);
4✔
305
    return vastXml;
4✔
306
  }
307

308
  return gamVastWrapper;
1✔
309
}
310

311
export async function getBase64BlobContent(blobUrl) {
312
  const response = await fetch(blobUrl);
1✔
313
  if (!response.ok) {
1!
UNCOV
314
    logError('Unable to fetch blob');
×
UNCOV
315
    throw new Error('Blob not found');
×
316
  }
317
  // Mechanism to handle cases where VAST tags are fetched
318
  // from a context where the blob resource is not accessible.
319
  // like IMA SDK iframe
320
  const blobContent = await response.text();
1✔
321
  const dataUrl = `data://text/xml;base64,${btoa(blobContent)}`;
1✔
322
  return dataUrl;
1✔
323
}
324

325
export { buildGamVideoUrl as buildDfpVideoUrl };
326

327
registerVideoSupport('gam', {
1✔
328
  buildVideoUrl: buildGamVideoUrl,
329
  getVastXml
330
});
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