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

prebid / Prebid.js / #283

20 Feb 2025 06:08PM UTC coverage: 90.519% (-0.005%) from 90.524%
#283

push

travis-ci

prebidjs-release
Prebid 9.31.0 release

41886 of 52458 branches covered (79.85%)

62085 of 68588 relevant lines covered (90.52%)

203.0 hits per line

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

84.47
/modules/debugging/bidInterceptor.js
1
import { Renderer } from '../../src/Renderer.js';
2
import { auctionManager } from '../../src/auctionManager.js';
3
import { BANNER, NATIVE, VIDEO } from '../../src/mediaTypes.js';
4
import {
5
  deepAccess,
6
  deepClone,
7
  delayExecution,
8
  mergeDeep,
9
  hasNonSerializableProperty
10
} from '../../src/utils.js';
11
import responseResolvers from './responses.js';
12

13
/**
14
 * @typedef {Number|String|boolean|null|undefined} Scalar
15
 */
16

17
export function BidInterceptor(opts = {}) {
×
18
  ({setTimeout: this.setTimeout = window.setTimeout.bind(window)} = opts);
50✔
19
  this.logger = opts.logger;
50✔
20
  this.rules = [];
50✔
21
}
22

23
Object.assign(BidInterceptor.prototype, {
1✔
24
  DEFAULT_RULE_OPTIONS: {
25
    delay: 0
26
  },
27
  serializeConfig(ruleDefs) {
28
    const isSerializable = (ruleDef, i) => {
7✔
29
      const serializable = !hasNonSerializableProperty(ruleDef);
21✔
30
      if (!serializable && !deepAccess(ruleDef, 'options.suppressWarnings')) {
21✔
31
        this.logger.logWarn(`Bid interceptor rule definition #${i + 1} contains non-serializable properties and will be lost after a refresh. Rule definition: `, ruleDef);
7✔
32
      }
33
      return serializable;
21✔
34
    }
35
    return ruleDefs.filter(isSerializable);
7✔
36
  },
37
  updateConfig(config) {
38
    this.rules = (config.intercept || []).map((ruleDef, i) => this.rule(ruleDef, i + 1))
49✔
39
  },
40
  /**
41
   * @typedef {Object} RuleOptions
42
   * @property {Number} [delay=0] delay between bid interception and mocking of response (to simulate network delay)
43
   * @property {boolean} [suppressWarnings=false] if enabled, do not warn about unserializable rules
44
   *
45
   * @typedef {Object} Rule
46
   * @property {Number} no rule number (used only as an identifier for logging)
47
   * @property {function({}, {}): boolean} match a predicate function that tests a bid against this rule
48
   * @property {ReplacerFn} replacer generator function for mock bid responses
49
   * @property {RuleOptions} options
50
   */
51

52
  /**
53
   * @param {{}} ruleDef
54
   * @param {Number} ruleNo
55
   * @returns {Rule}
56
   */
57
  rule(ruleDef, ruleNo) {
58
    return {
49✔
59
      no: ruleNo,
60
      match: this.matcher(ruleDef.when, ruleNo),
61
      replace: this.replacer(ruleDef.then, ruleNo),
62
      options: Object.assign({}, this.DEFAULT_RULE_OPTIONS, ruleDef.options),
63
      paapi: this.paapiReplacer(ruleDef.paapi || [], ruleNo)
87✔
64
    }
65
  },
66
  /**
67
   * @typedef {Function} MatchPredicate
68
   * @param {*} candidate a bid to match, or a portion of it if used inside an ObjectMather.
69
   * e.g. matcher((bid, bidRequest) => ....) or matcher({property: (property, bidRequest) => ...})
70
   * @param {BidRequest} bidRequest the request `candidate` belongs to
71
   * @returns {boolean}
72
   *
73
   * @typedef {{[key]: Scalar|RegExp|MatchPredicate|ObjectMatcher}} ObjectMatcher
74
   */
75

76
  /**
77
   * @param {MatchPredicate|ObjectMatcher} matchDef matcher definition
78
   * @param {Number} ruleNo
79
   * @returns {MatchPredicate} a predicate function that matches a bid against the given `matchDef`
80
   */
81
  matcher(matchDef, ruleNo) {
82
    if (typeof matchDef === 'function') {
49✔
83
      return matchDef;
15✔
84
    }
85
    if (typeof matchDef !== 'object') {
34!
86
      this.logger.logError(`Invalid 'when' definition for debug bid interceptor (in rule #${ruleNo})`);
×
87
      return () => false;
×
88
    }
89
    function matches(candidate, {ref = matchDef, args = []}) {
39!
90
      return Object.entries(ref).map(([key, val]) => {
46✔
91
        const cVal = candidate[key];
25✔
92
        if (val instanceof RegExp) {
25✔
93
          return val.exec(cVal) != null;
4✔
94
        }
95
        if (typeof val === 'function') {
21✔
96
          return !!val(cVal, ...args);
3✔
97
        }
98
        if (typeof val === 'object') {
18✔
99
          return matches(cVal, {ref: val, args});
7✔
100
        }
101
        return cVal === val;
11✔
102
      }).every((i) => i);
24✔
103
    }
104
    return (candidate, ...args) => matches(candidate, {args});
39✔
105
  },
106
  /**
107
   * @typedef {Function} ReplacerFn
108
   * @param {*} bid a bid that was intercepted
109
   * @param {BidRequest} bidRequest the request `bid` belongs to
110
   * @returns {*} the response to mock for `bid`, or a portion of it if used inside an ObjectReplacer.
111
   * e.g. replacer((bid, bidRequest) => mockResponse) or replacer({property: (bid, bidRequest) => mockProperty})
112
   *
113
   * @typedef {{[key]: ReplacerFn|ObjectReplacer|*}} ObjectReplacer
114
   */
115

116
  /**
117
   * @param {ReplacerFn|ObjectReplacer} replDef replacer definition
118
   * @param ruleNo
119
   * @return {ReplacerFn}
120
   */
121
  replacer(replDef, ruleNo) {
122
    if (replDef === null) {
49✔
123
      return () => null
1✔
124
    }
125
    replDef = replDef || {};
48✔
126
    let replFn;
127
    if (typeof replDef === 'function') {
48✔
128
      replFn = ({args}) => replDef(...args);
14✔
129
    } else if (typeof replDef !== 'object') {
34!
130
      this.logger.logError(`Invalid 'then' definition for debug bid interceptor (in rule #${ruleNo})`);
×
131
      replFn = () => ({});
×
132
    } else {
133
      replFn = ({args, ref = replDef}) => {
34✔
134
        const result = Array.isArray(ref) ? [] : {};
16✔
135
        Object.entries(ref).forEach(([key, val]) => {
16✔
136
          if (typeof val === 'function') {
15✔
137
            result[key] = val(...args);
4✔
138
          } else if (val != null && typeof val === 'object') {
11✔
139
            result[key] = replFn({args, ref: val})
8✔
140
          } else {
141
            result[key] = val;
3✔
142
          }
143
        });
144
        return result;
16✔
145
      }
146
    }
147
    return (bid, ...args) => {
48✔
148
      const response = this.responseDefaults(bid);
18✔
149
      mergeDeep(response, replFn({args: [bid, ...args]}));
18✔
150
      this.setDefaultAd(bid, response);
18✔
151
      response.isDebug = true;
18✔
152
      return response;
18✔
153
    }
154
  },
155

156
  paapiReplacer(paapiDef, ruleNo) {
157
    function wrap(configs = []) {
1✔
158
      return configs.map(config => {
20✔
159
        return Object.keys(config).some(k => !['config', 'igb'].includes(k))
14✔
160
          ? {config}
161
          : config
162
      });
163
    }
164
    if (Array.isArray(paapiDef)) {
49✔
165
      return () => wrap(paapiDef);
44✔
166
    } else if (typeof paapiDef === 'function') {
5!
167
      return (...args) => wrap(paapiDef(...args))
5✔
168
    } else {
169
      this.logger.logError(`Invalid 'paapi' definition for debug bid interceptor (in rule #${ruleNo})`);
×
170
    }
171
  },
172

173
  responseDefaults(bid) {
174
    const response = {
18✔
175
      requestId: bid.bidId,
176
      cpm: 3.5764,
177
      currency: 'EUR',
178
      width: 600,
179
      height: 500,
180
      ttl: 360,
181
      creativeId: 'mock-creative-id',
182
      netRevenue: false,
183
      meta: {}
184
    };
185

186
    if (!bid.mediaType) {
18!
187
      const adUnit = auctionManager.index.getAdUnit({adUnitId: bid.adUnitId}) || {mediaTypes: {}};
18✔
188
      response.mediaType = Object.keys(adUnit.mediaTypes)[0] || 'banner';
18✔
189
    }
190

191
    return response;
18✔
192
  },
193
  setDefaultAd(bid, bidResponse) {
194
    switch (bidResponse.mediaType) {
18!
195
      case VIDEO:
196
        if (!bidResponse.hasOwnProperty('vastXml') && !bidResponse.hasOwnProperty('vastUrl')) {
×
197
          bidResponse.vastXml = responseResolvers[VIDEO]();
×
198
          bidResponse.renderer = Renderer.install({
×
199
            url: 'https://cdn.jwplayer.com/libraries/l5MchIxB.js',
200
          });
201
          bidResponse.renderer.setRender(function (bid) {
×
202
            const player = window.jwplayer('player').setup({
×
203
              width: 640,
204
              height: 360,
205
              advertising: {
206
                client: 'vast',
207
                outstream: true,
208
                endstate: 'close'
209
              },
210
            });
211
            player.on('ready', function() {
×
212
              player.loadAdXml(bid.vastXml);
×
213
            });
214
          })
215
        }
216
        break;
×
217
      case NATIVE:
218
        if (!bidResponse.hasOwnProperty('native')) {
×
219
          bidResponse.native = responseResolvers[NATIVE](bid);
×
220
        }
221
        break;
×
222
      case BANNER:
223
      default:
224
        if (!bidResponse.hasOwnProperty('ad') && !bidResponse.hasOwnProperty('adUrl')) {
18!
225
          bidResponse.ad = responseResolvers[BANNER]();
18✔
226
        }
227
    }
228
  },
229
  /**
230
   * Match a candidate bid against all registered rules.
231
   *
232
   * @param {{}} candidate
233
   * @param args
234
   * @returns {Rule|undefined} the first matching rule, or undefined if no match was found.
235
   */
236
  match(candidate, ...args) {
237
    return this.rules.find((rule) => rule.match(candidate, ...args));
63✔
238
  },
239
  /**
240
   * Match a set of bids against all registered rules.
241
   *
242
   * @param bids
243
   * @param bidRequest
244
   * @returns {[{bid: *, rule: Rule}[], *[]]} a 2-tuple for matching bids (decorated with the matching rule) and
245
   * non-matching bids.
246
   */
247
  matchAll(bids, bidRequest) {
248
    const [matches, remainder] = [[], []];
11✔
249
    bids.forEach((bid) => {
11✔
250
      const rule = this.match(bid, bidRequest);
22✔
251
      if (rule != null) {
22✔
252
        matches.push({rule: rule, bid: bid});
10✔
253
      } else {
254
        remainder.push(bid);
12✔
255
      }
256
    })
257
    return [matches, remainder];
11✔
258
  },
259
  /**
260
   * Run a set of bids against all registered rules, filter out those that match,
261
   * and generate mock responses for them.
262
   *
263
   * @param {{}[]} bids?
264
   * @param {BidRequest} bidRequest
265
   * @param {function(*)} addBid called once for each mock response
266
   * @param addPaapiConfig called once for each mock PAAPI config
267
   * @param {function()} done called once after all mock responses have been run through `addBid`
268
   * @returns {{bids: {}[], bidRequest: {}} remaining bids that did not match any rule (this applies also to
269
   * bidRequest.bids)
270
   */
271
  intercept({bids, bidRequest, addBid, addPaapiConfig, done}) {
272
    if (bids == null) {
11✔
273
      bids = bidRequest.bids;
8✔
274
    }
275
    const [matches, remainder] = this.matchAll(bids, bidRequest);
11✔
276
    if (matches.length > 0) {
11✔
277
      const callDone = delayExecution(done, matches.length);
6✔
278
      matches.forEach((match) => {
6✔
279
        const mockResponse = match.rule.replace(match.bid, bidRequest);
10✔
280
        const mockPaapi = match.rule.paapi(match.bid, bidRequest);
10✔
281
        const delay = match.rule.options.delay;
10✔
282
        this.logger.logMessage(`Intercepted bid request (matching rule #${match.rule.no}), mocking response in ${delay}ms. Request, response, PAAPI configs:`, match.bid, mockResponse, mockPaapi)
10✔
283
        this.setTimeout(() => {
10✔
284
          mockResponse && addBid(mockResponse, match.bid);
10✔
285
          mockPaapi.forEach(cfg => addPaapiConfig(cfg, match.bid, bidRequest));
10✔
286
          callDone();
10✔
287
        }, delay)
288
      });
289
      bidRequest = deepClone(bidRequest);
6✔
290
      bids = bidRequest.bids = remainder;
6✔
291
    } else {
292
      this.setTimeout(done, 0);
5✔
293
    }
294
    return {bids, bidRequest};
11✔
295
  }
296
});
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