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

prebid / Prebid.js / #285

03 Mar 2025 04:58PM UTC coverage: 90.452% (-0.07%) from 90.523%
#285

push

travis-ci

prebidjs-release
Prebid 9.33.0 release

42181 of 52890 branches covered (79.75%)

62679 of 69295 relevant lines covered (90.45%)

222.46 hits per line

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

92.63
/modules/debugging/bidInterceptor.js
1
import {BANNER, VIDEO} from '../../src/mediaTypes.js';
2
import {deepAccess, deepClone, delayExecution, hasNonSerializableProperty, mergeDeep} from '../../src/utils.js';
3
import responseResolvers from './responses.js';
4

5
/**
6
 * @typedef {Number|String|boolean|null|undefined} Scalar
7
 */
8

9
export function BidInterceptor(opts = {}) {
×
10
  ({setTimeout: this.setTimeout = window.setTimeout.bind(window)} = opts);
50✔
11
  this.logger = opts.logger;
50✔
12
  this.rules = [];
50✔
13
}
14

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

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

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

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

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

166
  responseDefaults(bid) {
167
    const response = {
18✔
168
      requestId: bid.bidId,
169
      cpm: 3.5764,
170
      currency: 'EUR',
171
      ttl: 360,
172
      creativeId: 'mock-creative-id',
173
      netRevenue: false,
174
      meta: {}
175
    };
176

177
    if (!bid.mediaType) {
18!
178
      response.mediaType = Object.keys(bid.mediaTypes ?? {})[0] ?? BANNER;
18✔
179
    }
180
    let size;
181
    if (response.mediaType === BANNER) {
18!
182
      size = bid.mediaTypes?.banner?.sizes?.[0] ?? [300, 250];
18✔
183
    } else if (response.mediaType === VIDEO) {
×
184
      size = bid.mediaTypes?.video?.playerSize?.[0] ?? [600, 500];
×
185
    }
186
    if (Array.isArray(size)) {
18!
187
      ([response.width, response.height] = size);
18✔
188
    }
189
    return response;
18✔
190
  },
191
  /**
192
   * Match a candidate bid against all registered rules.
193
   *
194
   * @param {{}} candidate
195
   * @param args
196
   * @returns {Rule|undefined} the first matching rule, or undefined if no match was found.
197
   */
198
  match(candidate, ...args) {
199
    return this.rules.find((rule) => rule.match(candidate, ...args));
63✔
200
  },
201
  /**
202
   * Match a set of bids against all registered rules.
203
   *
204
   * @param bids
205
   * @param bidRequest
206
   * @returns {[{bid: *, rule: Rule}[], *[]]} a 2-tuple for matching bids (decorated with the matching rule) and
207
   * non-matching bids.
208
   */
209
  matchAll(bids, bidRequest) {
210
    const [matches, remainder] = [[], []];
11✔
211
    bids.forEach((bid) => {
11✔
212
      const rule = this.match(bid, bidRequest);
22✔
213
      if (rule != null) {
22✔
214
        matches.push({rule: rule, bid: bid});
10✔
215
      } else {
216
        remainder.push(bid);
12✔
217
      }
218
    })
219
    return [matches, remainder];
11✔
220
  },
221
  /**
222
   * Run a set of bids against all registered rules, filter out those that match,
223
   * and generate mock responses for them.
224
   *
225
   * @param {{}[]} bids?
226
   * @param {BidRequest} bidRequest
227
   * @param {function(*)} addBid called once for each mock response
228
   * @param addPaapiConfig called once for each mock PAAPI config
229
   * @param {function()} done called once after all mock responses have been run through `addBid`
230
   * @returns {{bids: {}[], bidRequest: {}} remaining bids that did not match any rule (this applies also to
231
   * bidRequest.bids)
232
   */
233
  intercept({bids, bidRequest, addBid, addPaapiConfig, done}) {
234
    if (bids == null) {
11✔
235
      bids = bidRequest.bids;
8✔
236
    }
237
    const [matches, remainder] = this.matchAll(bids, bidRequest);
11✔
238
    if (matches.length > 0) {
11✔
239
      const callDone = delayExecution(done, matches.length);
6✔
240
      matches.forEach((match) => {
6✔
241
        const mockResponse = match.rule.replace(match.bid, bidRequest);
10✔
242
        const mockPaapi = match.rule.paapi(match.bid, bidRequest);
10✔
243
        const delay = match.rule.options.delay;
10✔
244
        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✔
245
        this.setTimeout(() => {
10✔
246
          mockResponse && addBid(mockResponse, match.bid);
10✔
247
          mockPaapi.forEach(cfg => addPaapiConfig(cfg, match.bid, bidRequest));
10✔
248
          callDone();
10✔
249
        }, delay)
250
      });
251
      bidRequest = deepClone(bidRequest);
6✔
252
      bids = bidRequest.bids = remainder;
6✔
253
    } else {
254
      this.setTimeout(done, 0);
5✔
255
    }
256
    return {bids, bidRequest};
11✔
257
  }
258
});
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