diff --git a/modules/apsBidAdapter.js b/modules/apsBidAdapter.js
new file mode 100644
index 0000000000..732737d32b
--- /dev/null
+++ b/modules/apsBidAdapter.js
@@ -0,0 +1,367 @@
+import { isStr, isNumber, logWarn, logError } from '../src/utils.js';
+import { registerBidder } from '../src/adapters/bidderFactory.js';
+import { config } from '../src/config.js';
+import { BANNER, VIDEO } from '../src/mediaTypes.js';
+import { hasPurpose1Consent } from '../src/utils/gdpr.js';
+import { ortbConverter } from '../libraries/ortbConverter/converter.js';
+
+/**
+ * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest
+ * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid
+ * @typedef {import('../src/adapters/bidderFactory.js').ServerRequest} ServerRequest
+ * @typedef {import('../src/adapters/bidderFactory.js').BidderSpec} BidderSpec
+ */
+
+const GVLID = 793;
+export const ADAPTER_VERSION = '2.0.0';
+const BIDDER_CODE = 'aps';
+const AAX_ENDPOINT = 'https://web.ads.aps.amazon-adsystem.com/e/pb/bid';
+const DEFAULT_PREBID_CREATIVE_JS_URL =
+ 'https://client.aps.amazon-adsystem.com/prebid-creative.js';
+
+/**
+ * Records an event by pushing a CustomEvent onto a global queue.
+ * Creates an account-specific store on window._aps if needed.
+ * Automatically prefixes eventName with 'prebidAdapter/' if not already prefixed.
+ * Automatically appends '/didTrigger' if there is no third part provided in the event name.
+ *
+ * @param {string} eventName - The name of the event to record
+ * @param {object} data - Event data object, typically containing an 'error' property
+ */
+function record(eventName, data) {
+ // Check if telemetry is enabled
+ if (config.readConfig('aps.telemetry') === false) {
+ return;
+ }
+
+ // Automatically prefix eventName with 'prebidAdapter/' if not already prefixed
+ const prefixedEventName = eventName.startsWith('prebidAdapter/')
+ ? eventName
+ : `prebidAdapter/${eventName}`;
+
+ // Automatically append 'didTrigger' if there is no third part provided in the event name
+ const parts = prefixedEventName.split('/');
+ const finalEventName =
+ parts.length < 3 ? `${prefixedEventName}/didTrigger` : prefixedEventName;
+
+ const accountID = config.readConfig('aps.accountID');
+ if (!accountID) {
+ return;
+ }
+
+ window._aps = window._aps || new Map();
+ if (!window._aps.has(accountID)) {
+ window._aps.set(accountID, {
+ queue: [],
+ store: new Map(),
+ });
+ }
+
+ // Ensure analytics key exists unless error key is present
+ const detailData = { ...data };
+ if (!detailData.error) {
+ detailData.analytics = detailData.analytics || {};
+ }
+
+ window._aps.get(accountID).queue.push(
+ new CustomEvent(finalEventName, {
+ detail: {
+ ...detailData,
+ source: 'prebid-adapter',
+ libraryVersion: ADAPTER_VERSION,
+ },
+ })
+ );
+}
+
+/**
+ * Record and log a new error.
+ *
+ * @param {string} eventName - The name of the event to record
+ * @param {Error} err - Error object
+ * @param {any} data - Event data object
+ */
+function recordAndLogError(eventName, err, data) {
+ record(eventName, { ...data, error: err });
+ logError(err.message);
+}
+
+/**
+ * Validates whether a given account ID is valid.
+ *
+ * @param {string|number} accountID - The account ID to validate
+ * @returns {boolean} Returns true if the account ID is valid, false otherwise
+ */
+function isValidAccountID(accountID) {
+ // null/undefined are not acceptable
+ if (accountID == null) {
+ return false;
+ }
+
+ // Numbers are valid (including 0)
+ if (isNumber(accountID)) {
+ return true;
+ }
+
+ // Strings must have content after trimming
+ if (isStr(accountID)) {
+ return accountID.trim().length > 0;
+ }
+
+ // Other types are invalid
+ return false;
+}
+
+export const converter = ortbConverter({
+ context: {
+ netRevenue: true,
+ },
+
+ request(buildRequest, imps, bidderRequest, context) {
+ const request = buildRequest(imps, bidderRequest, context);
+
+ // Remove precise geo locations for privacy.
+ if (request?.device?.geo) {
+ delete request.device.geo.lat;
+ delete request.device.geo.lon;
+ }
+
+ if (request.user) {
+ // Remove sensitive user data.
+ delete request.user.gender;
+ delete request.user.yob;
+ // Remove both 'keywords' and alternate 'kwarry' if present.
+ delete request.user.keywords;
+ delete request.user.kwarry;
+ delete request.user.customdata;
+ delete request.user.geo;
+ delete request.user.data;
+ }
+
+ request.ext = request.ext ?? {};
+ request.ext.account = config.readConfig('aps.accountID');
+ request.ext.sdk = {
+ version: ADAPTER_VERSION,
+ source: 'prebid',
+ };
+ request.cur = request.cur ?? ['USD'];
+
+ if (!request.imp || !Array.isArray(request.imp)) {
+ return request;
+ }
+
+ request.imp.forEach((imp, index) => {
+ if (!imp) {
+ return; // continue to next iteration
+ }
+
+ if (!imp.banner) {
+ return; // continue to next iteration
+ }
+
+ const doesHWExist = imp.banner.w >= 0 && imp.banner.h >= 0;
+ const doesFormatExist =
+ Array.isArray(imp.banner.format) && imp.banner.format.length > 0;
+
+ if (doesHWExist || !doesFormatExist) {
+ return; // continue to next iteration
+ }
+
+ const { w, h } = imp.banner.format[0];
+
+ if (typeof w !== 'number' || typeof h !== 'number') {
+ return; // continue to next iteration
+ }
+
+ imp.banner.w = w;
+ imp.banner.h = h;
+ });
+
+ return request;
+ },
+
+ bidResponse(buildBidResponse, bid, context) {
+ let vastUrl;
+ if (bid.mtype === 2) {
+ vastUrl = bid.adm;
+ // Making sure no adm value is passed down to prevent issues with some renderers
+ delete bid.adm;
+ }
+
+ const bidResponse = buildBidResponse(bid, context);
+ if (bidResponse.mediaType === VIDEO) {
+ bidResponse.vastUrl = vastUrl;
+ }
+
+ return bidResponse;
+ },
+});
+
+/** @type {BidderSpec} */
+export const spec = {
+ code: BIDDER_CODE,
+ gvlid: GVLID,
+ supportedMediaTypes: [BANNER, VIDEO],
+
+ /**
+ * Validates the bid request.
+ * Always fires 100% of requests when account ID is valid.
+ * @param {object} bid
+ * @return {boolean}
+ */
+ isBidRequestValid: (bid) => {
+ record('isBidRequestValid');
+ try {
+ const accountID = config.readConfig('aps.accountID');
+ if (!isValidAccountID(accountID)) {
+ logWarn(`Invalid accountID: ${accountID}`);
+ return false;
+ }
+ return true;
+ } catch (err) {
+ err.message = `Error while validating bid request: ${err?.message}`;
+ recordAndLogError('isBidRequestValid/didError', err);
+ }
+ },
+
+ /**
+ * Constructs the server request for the bidder.
+ * @param {BidRequest[]} bidRequests
+ * @param {*} bidderRequest
+ * @return {ServerRequest}
+ */
+ buildRequests: (bidRequests, bidderRequest) => {
+ record('buildRequests');
+ try {
+ let endpoint = config.readConfig('aps.debugURL') ?? AAX_ENDPOINT;
+ // Append debug parameters to the URL if debug mode is enabled.
+ if (config.readConfig('aps.debug')) {
+ const debugQueryChar = endpoint.includes('?') ? '&' : '?';
+ const renderMethod = config.readConfig('aps.renderMethod');
+ if (renderMethod === 'fif') {
+ endpoint += debugQueryChar + 'amzn_debug_mode=fif&amzn_debug_mode=1';
+ } else {
+ endpoint += debugQueryChar + 'amzn_debug_mode=1';
+ }
+ }
+ return {
+ method: 'POST',
+ url: endpoint,
+ data: converter.toORTB({ bidRequests, bidderRequest }),
+ };
+ } catch (err) {
+ err.message = `Error while building bid request: ${err?.message}`;
+ recordAndLogError('buildRequests/didError', err);
+ }
+ },
+
+ /**
+ * Interprets the response from the server.
+ * Constructs a creative script to render the ad using a prebid creative JS.
+ * @param {*} response
+ * @param {ServerRequest} request
+ * @return {Bid[] | {bids: Bid[]}}
+ */
+ interpretResponse: (response, request) => {
+ record('interpretResponse');
+ try {
+ const interpretedResponse = converter.fromORTB({
+ response: response.body,
+ request: request.data,
+ });
+ const accountID = config.readConfig('aps.accountID');
+
+ const creativeUrl =
+ config.readConfig('aps.creativeURL') || DEFAULT_PREBID_CREATIVE_JS_URL;
+
+ interpretedResponse.bids.forEach((bid) => {
+ if (bid.mediaType !== VIDEO) {
+ delete bid.ad;
+ bid.ad = `
+`.trim();
+ }
+ });
+
+ return interpretedResponse.bids;
+ } catch (err) {
+ err.message = `Error while interpreting bid response: ${err?.message}`;
+ recordAndLogError('interpretResponse/didError', err);
+ }
+ },
+
+ /**
+ * Register user syncs to be processed during the shared user ID sync activity
+ *
+ * @param {Object} syncOptions - Options for user synchronization
+ * @param {Array} serverResponses - Array of bid responses
+ * @param {Object} gdprConsent - GDPR consent information
+ * @param {Object} uspConsent - USP consent information
+ * @returns {Array} Array of user sync objects
+ */
+ getUserSyncs: function (
+ syncOptions,
+ serverResponses,
+ gdprConsent,
+ uspConsent
+ ) {
+ record('getUserSyncs');
+ try {
+ if (hasPurpose1Consent(gdprConsent)) {
+ return serverResponses
+ .flatMap((res) => res?.body?.ext?.userSyncs ?? [])
+ .filter(
+ (s) =>
+ (s.type === 'iframe' && syncOptions.iframeEnabled) ||
+ (s.type === 'image' && syncOptions.pixelEnabled)
+ );
+ }
+ } catch (err) {
+ err.message = `Error while getting user syncs: ${err?.message}`;
+ recordAndLogError('getUserSyncs/didError', err);
+ }
+ },
+
+ onTimeout: (timeoutData) => {
+ record('onTimeout', { error: timeoutData });
+ },
+
+ onSetTargeting: (bid) => {
+ record('onSetTargeting');
+ },
+
+ onAdRenderSucceeded: (bid) => {
+ record('onAdRenderSucceeded');
+ },
+
+ onBidderError: (error) => {
+ record('onBidderError', { error });
+ },
+
+ onBidWon: (bid) => {
+ record('onBidWon');
+ },
+
+ onBidAttribute: (bid) => {
+ record('onBidAttribute');
+ },
+
+ onBidBillable: (bid) => {
+ record('onBidBillable');
+ },
+};
+
+registerBidder(spec);
diff --git a/modules/apsBidAdapter.md b/modules/apsBidAdapter.md
new file mode 100644
index 0000000000..1b772210af
--- /dev/null
+++ b/modules/apsBidAdapter.md
@@ -0,0 +1,84 @@
+# Overview
+
+```
+Module Name: APS Bidder Adapter
+Module Type: Bidder Adapter
+Maintainer: aps-prebid@amazon.com
+```
+
+# Description
+
+Connects to Amazon Publisher Services (APS) for bids.
+
+## Test Bids
+
+Please contact your APS Account Manager to learn more about our testing policies.
+
+# Usage
+
+## Prerequisites
+
+Add the account ID provided by APS to your configuration.
+
+```
+pbjs.setBidderConfig(
+ {
+ bidders: ['aps'],
+ config: {
+ aps: {
+ accountID: YOUR_APS_ACCOUNT_ID,
+ }
+ },
+ },
+ true // mergeConfig toggle
+);
+```
+
+## Ad Units
+
+## Banner
+
+```
+const adUnits = [
+ {
+ code: 'banner_div',
+ mediaTypes: {
+ banner: {
+ sizes: [[300, 250]],
+ },
+ },
+ bids: [{ bidder: 'aps' }],
+ },
+];
+```
+
+## Video
+
+Please select your preferred video renderer. The following example uses in-renderer-js:
+
+```
+const adUnits = [
+ {
+ code: 'video_div',
+ mediaTypes: {
+ video: {
+ playerSize: [400, 225],
+ context: 'outstream',
+ mimes: ['video/mp4'],
+ protocols: [1, 2, 3, 4, 5, 6, 7, 8],
+ minduration: 5,
+ maxduration: 30,
+ placement: 3,
+ },
+ },
+ bids: [{ bidder: 'aps' }],
+ renderer: {
+ url: 'https://cdn.jsdelivr.net/npm/in-renderer-js@1/dist/in-renderer.umd.min.js',
+ render(bid) {
+ new window.InRenderer().render('video_div', bid);
+ },
+ },
+ },
+];
+
+```
diff --git a/test/spec/modules/apsBidAdapter_spec.js b/test/spec/modules/apsBidAdapter_spec.js
new file mode 100644
index 0000000000..429572b034
--- /dev/null
+++ b/test/spec/modules/apsBidAdapter_spec.js
@@ -0,0 +1,1059 @@
+import sinon from 'sinon';
+import { expect } from 'chai';
+import { spec, ADAPTER_VERSION } from 'modules/apsBidAdapter';
+import { config } from 'src/config.js';
+
+/**
+ * Update config without rewriting the entire aps scope.
+ *
+ * Every call to setConfig() overwrites supplied values at the top level.
+ * e.g. if ortb2 is provided as a value, any previously-supplied ortb2
+ * values will disappear.
+ */
+const updateAPSConfig = (data) => {
+ const existingAPSConfig = config.readConfig('aps');
+ config.setConfig({
+ aps: {
+ ...existingAPSConfig,
+ ...data,
+ },
+ });
+};
+
+describe('apsBidAdapter', () => {
+ const accountID = 'test-account';
+
+ beforeEach(() => {
+ updateAPSConfig({ accountID });
+ });
+
+ afterEach(() => {
+ config.resetConfig();
+ delete window._aps;
+ });
+
+ describe('isBidRequestValid', () => {
+ it('should record prebidAdapter/isBidRequestValid/didTrigger event', () => {
+ spec.isBidRequestValid({});
+
+ const accountQueue = window._aps.get(accountID).queue;
+ expect(accountQueue).to.have.length(1);
+ expect(accountQueue[0].type).to.equal(
+ 'prebidAdapter/isBidRequestValid/didTrigger'
+ );
+ });
+
+ it('when no accountID provided, should not record event', () => {
+ updateAPSConfig({ accountID: undefined });
+ spec.isBidRequestValid({});
+
+ expect(window._aps).not.to.exist;
+ });
+
+ it('when telemetry is turned off, should not record event', () => {
+ updateAPSConfig({ telemetry: false });
+ spec.isBidRequestValid({});
+
+ expect(window._aps).not.to.exist;
+ });
+
+ [
+ { accountID: undefined },
+ { accountID: null },
+ { accountID: [] },
+ { accountID: { key: 'value' } },
+ { accountID: true },
+ { accountID: false },
+ ].forEach((scenario) => {
+ it(`when accountID is ${JSON.stringify(scenario.accountID)}, should return false`, () => {
+ updateAPSConfig({ accountID: scenario.accountID });
+ const actual = spec.isBidRequestValid({});
+ expect(actual).to.equal(false);
+ });
+ });
+
+ it('when accountID is a number, should return true', () => {
+ updateAPSConfig({ accountID: 1234 });
+ const actual = spec.isBidRequestValid({});
+ expect(actual).to.equal(true);
+ });
+
+ it('when accountID is a string, should return true', () => {
+ updateAPSConfig({ accountID: '1234' });
+ const actual = spec.isBidRequestValid({});
+ expect(actual).to.equal(true);
+ });
+ });
+
+ describe('buildRequests', () => {
+ let bidRequests, bidderRequest;
+
+ beforeEach(() => {
+ bidRequests = [
+ {
+ bidId: 'bid1',
+ adUnitCode: 'adunit1',
+ mediaTypes: { banner: { sizes: [[300, 250]] } },
+ params: {},
+ },
+ {
+ bidId: 'bid2',
+ code: 'video_div',
+ mediaTypes: {
+ video: {
+ playerSize: [400, 225],
+ context: 'outstream',
+ mimes: ['video/mp4'],
+ protocols: [1, 2, 3, 4, 5, 6, 7, 8],
+ minduration: 5,
+ maxduration: 30,
+ placement: 3,
+ },
+ },
+ bids: [{ bidder: 'aps' }],
+ },
+ ];
+ bidderRequest = {
+ bidderCode: 'aps',
+ auctionId: 'auction1',
+ bidderRequestId: 'request1',
+ };
+ });
+
+ it('should record prebidAdapter/buildRequests/didTrigger event', () => {
+ spec.buildRequests(bidRequests, bidderRequest);
+
+ const accountQueue = window._aps.get(accountID).queue;
+ expect(accountQueue).to.have.length(1);
+ expect(accountQueue[0].type).to.equal(
+ 'prebidAdapter/buildRequests/didTrigger'
+ );
+ });
+
+ it('when no accountID provided, should not record event', () => {
+ updateAPSConfig({ accountID: undefined });
+ spec.buildRequests(bidRequests, bidderRequest);
+
+ expect(window._aps).not.to.exist;
+ });
+
+ it('when telemetry is turned off, should not record event', () => {
+ updateAPSConfig({ telemetry: false });
+ spec.buildRequests(bidRequests, bidderRequest);
+
+ expect(window._aps).not.to.exist;
+ });
+
+ it('should return server request with default endpoint', () => {
+ const result = spec.buildRequests(bidRequests, bidderRequest);
+
+ expect(result.method).to.equal('POST');
+ expect(result.url).to.equal(
+ 'https://web.ads.aps.amazon-adsystem.com/e/pb/bid'
+ );
+ expect(result.data).to.exist;
+ });
+
+ it('should return server request with properly formatted impressions', () => {
+ const result = spec.buildRequests(bidRequests, bidderRequest);
+
+ expect(result.data.imp.length).to.equal(2);
+ expect(result.data.imp[0]).to.deep.equal({
+ banner: { format: [{ h: 250, w: 300 }], h: 250, topframe: 0, w: 300 },
+ id: 'bid1',
+ secure: 1,
+ });
+ expect(result.data.imp[1]).to.deep.equal({
+ id: 'bid2',
+ secure: 1,
+ ...(FEATURES.VIDEO && {
+ video: {
+ h: 225,
+ maxduration: 30,
+ mimes: ['video/mp4'],
+ minduration: 5,
+ placement: 3,
+ protocols: [1, 2, 3, 4, 5, 6, 7, 8],
+ w: 400,
+ },
+ }),
+ });
+ });
+
+ it('when debugURL is provided, should use custom debugURL', () => {
+ updateAPSConfig({ debugURL: 'https://example.com' });
+
+ const result = spec.buildRequests(bidRequests, bidderRequest);
+
+ expect(result.url).to.equal('https://example.com');
+ });
+
+ it('should convert bid requests to ORTB format with account', () => {
+ const result = spec.buildRequests(bidRequests, bidderRequest);
+
+ expect(result.data).to.be.an('object');
+ expect(result.data.ext).to.exist;
+ expect(result.data.ext.account).to.equal(accountID);
+ });
+
+ it('should include ADAPTER_VERSION in request data', () => {
+ const result = spec.buildRequests(bidRequests, bidderRequest);
+
+ expect(result.data.ext.sdk.version).to.equal(ADAPTER_VERSION);
+ expect(result.data.ext.sdk.source).to.equal('prebid');
+ });
+
+ it('when accountID is not provided, should convert bid requests to ORTB format with no account', () => {
+ updateAPSConfig({ accountID: undefined });
+
+ const result = spec.buildRequests(bidRequests, bidderRequest);
+
+ expect(result.data).to.be.an('object');
+ expect(result.data.ext).to.exist;
+ expect(result.data.ext.account).to.equal(undefined);
+ });
+
+ it('should remove sensitive geo data from device', () => {
+ bidderRequest.ortb2 = {
+ device: {
+ geo: {
+ lat: 37.7749,
+ lon: -122.4194,
+ country: 'US',
+ },
+ },
+ };
+
+ const result = spec.buildRequests(bidRequests, bidderRequest);
+
+ expect(result.data.device.geo.lat).to.be.undefined;
+ expect(result.data.device.geo.lon).to.be.undefined;
+ expect(result.data.device.geo.country).to.equal('US');
+ });
+
+ it('should remove sensitive user data', () => {
+ bidderRequest.ortb2 = {
+ user: {
+ gender: 'M',
+ yob: 1990,
+ keywords: 'sports,tech',
+ kwarry: 'alternate keywords',
+ customdata: 'custom user data',
+ geo: { lat: 37.7749, lon: -122.4194 },
+ data: [{ id: 'segment1' }],
+ id: 'user123',
+ },
+ };
+
+ const result = spec.buildRequests(bidRequests, bidderRequest);
+
+ expect(result.data.user.gender).to.be.undefined;
+ expect(result.data.user.yob).to.be.undefined;
+ expect(result.data.user.keywords).to.be.undefined;
+ expect(result.data.user.kwarry).to.be.undefined;
+ expect(result.data.user.customdata).to.be.undefined;
+ expect(result.data.user.geo).to.be.undefined;
+ expect(result.data.user.data).to.be.undefined;
+ expect(result.data.user.id).to.equal('user123');
+ });
+
+ it('should set default currency to USD', () => {
+ const result = spec.buildRequests(bidRequests, bidderRequest);
+
+ expect(result.data.cur).to.deep.equal(['USD']);
+ });
+
+ [
+ { imp: undefined },
+ { imp: null },
+ { imp: 'not an array' },
+ { imp: 123 },
+ { imp: true },
+ { imp: false },
+ ].forEach((scenario) => {
+ it(`when imp is ${JSON.stringify(scenario.imp)}, should send data`, () => {
+ bidderRequest.ortb2 = {
+ imp: scenario.imp,
+ };
+
+ const result = spec.buildRequests(bidRequests, bidderRequest);
+
+ expect(result.data.imp).to.equal(scenario.imp);
+ });
+ });
+
+ [
+ { imp: [null] },
+ { imp: [undefined] },
+ { imp: [null, {}] },
+ { imp: [{}, null] },
+ { imp: [undefined, {}] },
+ { imp: [{}, undefined] },
+ ].forEach((scenario, scenarioIndex) => {
+ it(`when imp array contains null/undefined at index, should send data - scenario ${scenarioIndex}`, () => {
+ bidRequests = [];
+ bidderRequest.ortb2 = { imp: scenario.imp };
+
+ const result = spec.buildRequests(bidRequests, bidderRequest);
+
+ expect(result.data.imp).to.deep.equal(scenario.imp);
+ });
+ });
+
+ [
+ { w: 'invalid', h: 250 },
+ { w: 300, h: 'invalid' },
+ { w: null, h: 250 },
+ { w: 300, h: undefined },
+ { w: true, h: 250 },
+ { w: 300, h: false },
+ { w: {}, h: 250 },
+ { w: 300, h: [] },
+ ].forEach((scenario) => {
+ it(`when imp array contains banner object with invalid format (h: "${scenario.h}", w: "${scenario.w}"), should send data`, () => {
+ const { w, h } = scenario;
+ const invalidBannerObj = {
+ banner: {
+ format: [
+ { w, h },
+ { w: 300, h: 250 },
+ ],
+ },
+ };
+ const imp = [
+ { banner: { format: [{ w: 300, h: 250 }] } },
+ { video: { w: 300, h: undefined } },
+ invalidBannerObj,
+ { video: { w: undefined, h: 300 } },
+ ];
+ bidRequests = [];
+ bidderRequest.ortb2 = { imp };
+
+ const result = spec.buildRequests(bidRequests, bidderRequest);
+
+ expect(result.data.imp).to.deep.equal(imp);
+ });
+ });
+
+ describe('when debug mode is enabled', () => {
+ beforeEach(() => {
+ updateAPSConfig({ debug: true });
+ });
+
+ it('should append debug parameters', () => {
+ const result = spec.buildRequests(bidRequests, bidderRequest);
+
+ expect(result.url).to.equal(
+ 'https://web.ads.aps.amazon-adsystem.com/e/pb/bid?amzn_debug_mode=1'
+ );
+ });
+
+ it('when using custom endpoint, should append debug parameters', () => {
+ updateAPSConfig({ debugURL: 'https://example.com' });
+
+ const result = spec.buildRequests(bidRequests, bidderRequest);
+
+ expect(result.url).to.equal('https://example.com?amzn_debug_mode=1');
+ });
+
+ it('when endpoint has existing query params, should append debug parameters with &', () => {
+ updateAPSConfig({
+ debugURL: 'https://example.com?existing=param',
+ });
+
+ const result = spec.buildRequests(bidRequests, bidderRequest);
+
+ expect(result.url).to.equal(
+ 'https://example.com?existing=param&amzn_debug_mode=1'
+ );
+ });
+
+ describe('when renderMethod is fif', () => {
+ beforeEach(() => {
+ updateAPSConfig({ renderMethod: 'fif' });
+ });
+
+ it('when renderMethod is fif, should append fif debug parameters', () => {
+ const result = spec.buildRequests(bidRequests, bidderRequest);
+
+ expect(result.url).to.equal(
+ 'https://web.ads.aps.amazon-adsystem.com/e/pb/bid?amzn_debug_mode=fif&amzn_debug_mode=1'
+ );
+ });
+ });
+ });
+ });
+
+ describe('interpretResponse', () => {
+ const impid = '32adcfab8e54178';
+ let response, request, bidRequests, bidderRequest;
+
+ beforeEach(() => {
+ bidRequests = [
+ {
+ bidder: 'aps',
+ params: {},
+ ortb2Imp: { ext: { data: {} } },
+ mediaTypes: { banner: { sizes: [[300, 250]] } },
+ adUnitCode: 'display-ad',
+ adUnitId: '57661158-f277-4061-bbfc-532b6f811c7b',
+ sizes: [[300, 250]],
+ bidId: impid,
+ bidderRequestId: '2a1ec2d1ccea318',
+ },
+ ];
+ bidderRequest = {
+ bidderCode: 'aps',
+ auctionId: null,
+ bidderRequestId: '2a1ec2d1ccea318',
+ bids: [
+ {
+ bidder: 'aps',
+ params: {},
+ ortb2Imp: { ext: { data: {} } },
+ mediaTypes: { banner: { sizes: [[300, 250]] } },
+ adUnitCode: 'display-ad',
+ adUnitId: '57661158-f277-4061-bbfc-532b6f811c7b',
+ sizes: [[300, 250]],
+ bidId: impid,
+ bidderRequestId: '2a1ec2d1ccea318',
+ },
+ ],
+ start: 1758899825329,
+ };
+
+ request = spec.buildRequests(bidRequests, bidderRequest);
+
+ response = {
+ body: {
+ id: '53d4dda2-cf3d-455a-8554-48f051ca4ad3',
+ cur: 'USD',
+ seatbid: [
+ {
+ bid: [
+ {
+ mtype: 1,
+ id: 'jp45_n29nkvhfuttv0rhl5iaaagvz_t54weaaaxzaqbhchnfdhhux2jpzdigicbhchnfdhhux2ltcdegicdpqbra',
+ adid: 'eaayacognuhq9jcfs8rwkoyyhmwtke4e4jmnrjcx.ywnbprnvr0ybkk6wpu_',
+ price: 5.5,
+ impid,
+ crid: 'amazon-test-ad',
+ w: 300,
+ h: 250,
+ exp: 3600,
+ },
+ ],
+ },
+ ],
+ },
+ headers: {},
+ };
+ });
+
+ it('should record prebidAdapter/interpretResponse/didTrigger event', () => {
+ spec.interpretResponse(response, request);
+
+ const accountQueue = window._aps.get(accountID).queue;
+ expect(accountQueue).to.have.length(2);
+ expect(accountQueue[0].type).to.equal(
+ 'prebidAdapter/buildRequests/didTrigger'
+ );
+ expect(accountQueue[1].type).to.equal(
+ 'prebidAdapter/interpretResponse/didTrigger'
+ );
+ });
+
+ it('should return interpreted bids from ORTB response', () => {
+ const result = spec.interpretResponse(response, request);
+
+ expect(result).to.be.an('array');
+ expect(result.length).to.equal(1);
+ });
+
+ it('should include accountID in creative script', () => {
+ updateAPSConfig({ accountID: accountID });
+
+ const result = spec.interpretResponse(response, request);
+
+ expect(result).to.have.length(1);
+ expect(result[0].ad).to.include("const accountID = 'test-account'");
+ });
+
+ it('when creativeURL is provided, should use custom creative URL', () => {
+ updateAPSConfig({
+ creativeURL: 'https://custom-creative.com/script.js',
+ });
+
+ const result = spec.interpretResponse(response, request);
+
+ expect(result).to.have.length(1);
+ expect(result[0].ad).to.include(
+ 'src="https://custom-creative.com/script.js"'
+ );
+ });
+
+ it('should use default creative URL when not provided', () => {
+ const result = spec.interpretResponse(response, request);
+
+ expect(result).to.have.length(1);
+ expect(result[0].ad).to.include(
+ 'src="https://client.aps.amazon-adsystem.com/prebid-creative.js"'
+ );
+ });
+
+ describe('when bid mediaType is VIDEO', () => {
+ beforeEach(() => {
+ response.body.seatbid[0].bid[0].mtype = 2;
+ });
+
+ it('should not inject creative script for video bids', () => {
+ const result = spec.interpretResponse(response, request);
+
+ expect(result).to.have.length(1);
+ expect(result[0].ad).to.be.undefined;
+ });
+ });
+
+ describe('when bid mediaType is not VIDEO', () => {
+ it('should inject creative script for non-video bids', () => {
+ const result = spec.interpretResponse(response, request);
+
+ expect(result).to.have.length(1);
+ expect(result[0].ad).to.include('