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

prebid / Prebid.js / 21071502943

16 Jan 2026 03:22PM UTC coverage: 96.214% (+5.3%) from 90.95%
21071502943

push

github

9464c9
web-flow
APS Bid Adapter Initial Release (#14255)

**Overview**
------------
APS (Amazon Publisher Services) bid adapter initial open source release.

**Changes**
-----------
- (feat) Banner ad support
- (feat) Video ad support
- (feat) iframe user sync support
- (feat) Telemetry and analytics
- (docs) Integration guide

Co-authored-by: Chris Huie <3444727+ChrisHuie@users.noreply.github.com>

41571 of 51104 branches covered (81.35%)

502 of 513 new or added lines in 2 files covered. (97.86%)

2 existing lines in 1 file now uncovered.

208327 of 216525 relevant lines covered (96.21%)

71.44 hits per line

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

90.91
/modules/apsBidAdapter.js
1
import { isStr, isNumber, logWarn, logError } from '../src/utils.js';
1✔
2
import { registerBidder } from '../src/adapters/bidderFactory.js';
3
import { config } from '../src/config.js';
4
import { BANNER, VIDEO } from '../src/mediaTypes.js';
5
import { hasPurpose1Consent } from '../src/utils/gdpr.js';
6
import { ortbConverter } from '../libraries/ortbConverter/converter.js';
7

8
/**
9
 * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest
10
 * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid
11
 * @typedef {import('../src/adapters/bidderFactory.js').ServerRequest} ServerRequest
12
 * @typedef {import('../src/adapters/bidderFactory.js').BidderSpec} BidderSpec
13
 */
14

15
const GVLID = 793;
1✔
16
export const ADAPTER_VERSION = '2.0.0';
1✔
17
const BIDDER_CODE = 'aps';
1✔
18
const AAX_ENDPOINT = 'https://web.ads.aps.amazon-adsystem.com/e/pb/bid';
1✔
19
const DEFAULT_PREBID_CREATIVE_JS_URL =
20
  'https://client.aps.amazon-adsystem.com/prebid-creative.js';
1✔
21

22
/**
23
 * Records an event by pushing a CustomEvent onto a global queue.
24
 * Creates an account-specific store on window._aps if needed.
25
 * Automatically prefixes eventName with 'prebidAdapter/' if not already prefixed.
26
 * Automatically appends '/didTrigger' if there is no third part provided in the event name.
27
 *
28
 * @param {string} eventName - The name of the event to record
29
 * @param {object} data - Event data object, typically containing an 'error' property
30
 */
31
function record(eventName, data) {
32
  // Check if telemetry is enabled
33
  if (config.readConfig('aps.telemetry') === false) {
103✔
34
    return;
10✔
35
  }
36

37
  // Automatically prefix eventName with 'prebidAdapter/' if not already prefixed
38
  const prefixedEventName = eventName.startsWith('prebidAdapter/')
93!
39
    ? eventName
40
    : `prebidAdapter/${eventName}`;
41

42
  // Automatically append 'didTrigger' if there is no third part provided in the event name
43
  const parts = prefixedEventName.split('/');
93✔
44
  const finalEventName =
45
    parts.length < 3 ? `${prefixedEventName}/didTrigger` : prefixedEventName;
93!
46

47
  const accountID = config.readConfig('aps.accountID');
93✔
48
  if (!accountID) {
93✔
49
    return;
14✔
50
  }
51

52
  window._aps = window._aps || new Map();
79✔
53
  if (!window._aps.has(accountID)) {
79✔
54
    window._aps.set(accountID, {
71✔
55
      queue: [],
56
      store: new Map(),
57
    });
58
  }
59

60
  // Ensure analytics key exists unless error key is present
61
  const detailData = { ...data };
79✔
62
  if (!detailData.error) {
79✔
63
    detailData.analytics = detailData.analytics || {};
77✔
64
  }
65

66
  window._aps.get(accountID).queue.push(
79✔
67
    new CustomEvent(finalEventName, {
68
      detail: {
69
        ...detailData,
70
        source: 'prebid-adapter',
71
        libraryVersion: ADAPTER_VERSION,
72
      },
73
    })
74
  );
75
}
76

77
/**
78
 * Record and log a new error.
79
 *
80
 * @param {string} eventName - The name of the event to record
81
 * @param {Error} err - Error object
82
 * @param {any} data - Event data object
83
 */
84
function recordAndLogError(eventName, err, data) {
NEW
85
  record(eventName, { ...data, error: err });
×
NEW
86
  logError(err.message);
×
87
}
88

89
/**
90
 * Validates whether a given account ID is valid.
91
 *
92
 * @param {string|number} accountID - The account ID to validate
93
 * @returns {boolean} Returns true if the account ID is valid, false otherwise
94
 */
95
function isValidAccountID(accountID) {
96
  // null/undefined are not acceptable
97
  if (accountID == null) {
11✔
98
    return false;
3✔
99
  }
100

101
  // Numbers are valid (including 0)
102
  if (isNumber(accountID)) {
8✔
103
    return true;
1✔
104
  }
105

106
  // Strings must have content after trimming
107
  if (isStr(accountID)) {
7✔
108
    return accountID.trim().length > 0;
3✔
109
  }
110

111
  // Other types are invalid
112
  return false;
4✔
113
}
114

115
export const converter = ortbConverter({
1✔
116
  context: {
117
    netRevenue: true,
118
  },
119

120
  request(buildRequest, imps, bidderRequest, context) {
121
    const request = buildRequest(imps, bidderRequest, context);
44✔
122

123
    // Remove precise geo locations for privacy.
124
    if (request?.device?.geo) {
44✔
125
      delete request.device.geo.lat;
1✔
126
      delete request.device.geo.lon;
1✔
127
    }
128

129
    if (request.user) {
44✔
130
      // Remove sensitive user data.
131
      delete request.user.gender;
1✔
132
      delete request.user.yob;
1✔
133
      // Remove both 'keywords' and alternate 'kwarry' if present.
134
      delete request.user.keywords;
1✔
135
      delete request.user.kwarry;
1✔
136
      delete request.user.customdata;
1✔
137
      delete request.user.geo;
1✔
138
      delete request.user.data;
1✔
139
    }
140

141
    request.ext = request.ext ?? {};
44✔
142
    request.ext.account = config.readConfig('aps.accountID');
44✔
143
    request.ext.sdk = {
44✔
144
      version: ADAPTER_VERSION,
145
      source: 'prebid',
146
    };
147
    request.cur = request.cur ?? ['USD'];
44✔
148

149
    if (!request.imp || !Array.isArray(request.imp)) {
44✔
150
      return request;
6✔
151
    }
152

153
    request.imp.forEach((imp, index) => {
38✔
154
      if (!imp) {
82✔
155
        return; // continue to next iteration
6✔
156
      }
157

158
      if (!imp.banner) {
76✔
159
        return; // continue to next iteration
36✔
160
      }
161

162
      const doesHWExist = imp.banner.w >= 0 && imp.banner.h >= 0;
40!
163
      const doesFormatExist =
164
        Array.isArray(imp.banner.format) && imp.banner.format.length > 0;
40✔
165

166
      if (doesHWExist || !doesFormatExist) {
40!
NEW
167
        return; // continue to next iteration
×
168
      }
169

170
      const { w, h } = imp.banner.format[0];
40✔
171

172
      if (typeof w !== 'number' || typeof h !== 'number') {
40✔
173
        return; // continue to next iteration
8✔
174
      }
175

176
      imp.banner.w = w;
32✔
177
      imp.banner.h = h;
32✔
178
    });
179

180
    return request;
38✔
181
  },
182

183
  bidResponse(buildBidResponse, bid, context) {
184
    let vastUrl;
185
    if (bid.mtype === 2) {
8✔
186
      vastUrl = bid.adm;
1✔
187
      // Making sure no adm value is passed down to prevent issues with some renderers
188
      delete bid.adm;
1✔
189
    }
190

191
    const bidResponse = buildBidResponse(bid, context);
8✔
192
    if (bidResponse.mediaType === VIDEO) {
8✔
193
      bidResponse.vastUrl = vastUrl;
1✔
194
    }
195

196
    return bidResponse;
8✔
197
  },
198
});
199

200
/** @type {BidderSpec} */
201
export const spec = {
1✔
202
  code: BIDDER_CODE,
203
  gvlid: GVLID,
204
  supportedMediaTypes: [BANNER, VIDEO],
205

206
  /**
207
   * Validates the bid request.
208
   * Always fires 100% of requests when account ID is valid.
209
   * @param {object} bid
210
   * @return {boolean}
211
   */
212
  isBidRequestValid: (bid) => {
213
    record('isBidRequestValid');
11✔
214
    try {
11✔
215
      const accountID = config.readConfig('aps.accountID');
11✔
216
      if (!isValidAccountID(accountID)) {
11✔
217
        logWarn(`Invalid accountID: ${accountID}`);
7✔
218
        return false;
7✔
219
      }
220
      return true;
4✔
221
    } catch (err) {
NEW
222
      err.message = `Error while validating bid request: ${err?.message}`;
×
NEW
223
      recordAndLogError('isBidRequestValid/didError', err);
×
224
    }
225
  },
226

227
  /**
228
   * Constructs the server request for the bidder.
229
   * @param {BidRequest[]} bidRequests
230
   * @param {*} bidderRequest
231
   * @return {ServerRequest}
232
   */
233
  buildRequests: (bidRequests, bidderRequest) => {
234
    record('buildRequests');
44✔
235
    try {
44✔
236
      let endpoint = config.readConfig('aps.debugURL') ?? AAX_ENDPOINT;
44✔
237
      // Append debug parameters to the URL if debug mode is enabled.
238
      if (config.readConfig('aps.debug')) {
44✔
239
        const debugQueryChar = endpoint.includes('?') ? '&' : '?';
4✔
240
        const renderMethod = config.readConfig('aps.renderMethod');
4✔
241
        if (renderMethod === 'fif') {
4✔
242
          endpoint += debugQueryChar + 'amzn_debug_mode=fif&amzn_debug_mode=1';
1✔
243
        } else {
244
          endpoint += debugQueryChar + 'amzn_debug_mode=1';
3✔
245
        }
246
      }
247
      return {
44✔
248
        method: 'POST',
249
        url: endpoint,
250
        data: converter.toORTB({ bidRequests, bidderRequest }),
251
      };
252
    } catch (err) {
NEW
253
      err.message = `Error while building bid request: ${err?.message}`;
×
NEW
254
      recordAndLogError('buildRequests/didError', err);
×
255
    }
256
  },
257

258
  /**
259
   * Interprets the response from the server.
260
   * Constructs a creative script to render the ad using a prebid creative JS.
261
   * @param {*} response
262
   * @param {ServerRequest} request
263
   * @return {Bid[] | {bids: Bid[]}}
264
   */
265
  interpretResponse: (response, request) => {
266
    record('interpretResponse');
8✔
267
    try {
8✔
268
      const interpretedResponse = converter.fromORTB({
8✔
269
        response: response.body,
270
        request: request.data,
271
      });
272
      const accountID = config.readConfig('aps.accountID');
8✔
273

274
      const creativeUrl =
275
        config.readConfig('aps.creativeURL') || DEFAULT_PREBID_CREATIVE_JS_URL;
8✔
276

277
      interpretedResponse.bids.forEach((bid) => {
8✔
278
        if (bid.mediaType !== VIDEO) {
8✔
279
          delete bid.ad;
7✔
280
          bid.ad = `<script src="${creativeUrl}"></script>
7✔
281
<script>
282
  const accountID = '${accountID}';
283
  window._aps = window._aps || new Map();
284
  if (!window._aps.has(accountID)) {
285
    window._aps.set(accountID, { queue: [], store: new Map([['listeners', new Map()]]) });
286
  }
287
  window._aps.get(accountID).queue.push(
288
    new CustomEvent('prebid/creative/render', {
289
      detail: {
290
        aaxResponse: '${btoa(JSON.stringify(response.body))}',
291
        seatBidId: ${JSON.stringify(bid.seatBidId)}
292
      }
293
    })
294
  );
295
</script>`.trim();
296
        }
297
      });
298

299
      return interpretedResponse.bids;
8✔
300
    } catch (err) {
NEW
301
      err.message = `Error while interpreting bid response: ${err?.message}`;
×
NEW
302
      recordAndLogError('interpretResponse/didError', err);
×
303
    }
304
  },
305

306
  /**
307
   * Register user syncs to be processed during the shared user ID sync activity
308
   *
309
   * @param {Object} syncOptions - Options for user synchronization
310
   * @param {Array} serverResponses - Array of bid responses
311
   * @param {Object} gdprConsent - GDPR consent information
312
   * @param {Object} uspConsent - USP consent information
313
   * @returns {Array} Array of user sync objects
314
   */
315
  getUserSyncs: function (
316
    syncOptions,
317
    serverResponses,
318
    gdprConsent,
319
    uspConsent
320
  ) {
321
    record('getUserSyncs');
19✔
322
    try {
19✔
323
      if (hasPurpose1Consent(gdprConsent)) {
19✔
324
        return serverResponses
18✔
325
          .flatMap((res) => res?.body?.ext?.userSyncs ?? [])
18✔
326
          .filter(
327
            (s) =>
328
              (s.type === 'iframe' && syncOptions.iframeEnabled) ||
61✔
329
              (s.type === 'image' && syncOptions.pixelEnabled)
330
          );
331
      }
332
    } catch (err) {
NEW
333
      err.message = `Error while getting user syncs: ${err?.message}`;
×
NEW
334
      recordAndLogError('getUserSyncs/didError', err);
×
335
    }
336
  },
337

338
  onTimeout: (timeoutData) => {
339
    record('onTimeout', { error: timeoutData });
3✔
340
  },
341

342
  onSetTargeting: (bid) => {
343
    record('onSetTargeting');
3✔
344
  },
345

346
  onAdRenderSucceeded: (bid) => {
347
    record('onAdRenderSucceeded');
3✔
348
  },
349

350
  onBidderError: (error) => {
351
    record('onBidderError', { error });
3✔
352
  },
353

354
  onBidWon: (bid) => {
355
    record('onBidWon');
3✔
356
  },
357

358
  onBidAttribute: (bid) => {
359
    record('onBidAttribute');
3✔
360
  },
361

362
  onBidBillable: (bid) => {
363
    record('onBidBillable');
3✔
364
  },
365
};
366

367
registerBidder(spec);
1✔
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