From e8a36867511d744eaefdc6ef4aa5903d7cf5c246 Mon Sep 17 00:00:00 2001 From: prebid-compas Date: Thu, 9 Jul 2026 10:48:46 +0300 Subject: [PATCH 1/3] Implementation of the Synapse HX adapter --- modules/synapseHxBidAdapter.js | 161 ++++++ modules/synapseHxBidAdapter.md | 76 +++ test/spec/modules/synapseHxBidAdapter_spec.js | 485 ++++++++++++++++++ 3 files changed, 722 insertions(+) create mode 100644 modules/synapseHxBidAdapter.js create mode 100644 modules/synapseHxBidAdapter.md create mode 100644 test/spec/modules/synapseHxBidAdapter_spec.js diff --git a/modules/synapseHxBidAdapter.js b/modules/synapseHxBidAdapter.js new file mode 100644 index 00000000000..2f382ff6740 --- /dev/null +++ b/modules/synapseHxBidAdapter.js @@ -0,0 +1,161 @@ +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { BANNER, VIDEO } from '../src/mediaTypes.js'; +import { logError, isFn, isPlainObject, formatQS } from '../src/utils.js'; +import { ortbConverter } from '../libraries/ortbConverter/converter.js'; +import { toOrtb26 } from '../libraries/ortb2.5Translator/translator.js'; +import { getUserSyncParams } from '../libraries/userSyncUtils/userSyncUtils.js'; + +const BIDDER_CODE = 'synapsehx'; +const METHOD = 'POST'; +const ENDPOINT_URL = `https://rtb.hx.compasonline.com/pbjs`; + +function getMediaType(bid) { + if (bid.mtype) { + const mtypeToMediaType = { 1: BANNER, 2: VIDEO }; + return mtypeToMediaType[bid.mtype]; + } + return bid?.ext?.prebid?.type; +} + +const converter = ortbConverter({ + imp(buildImp, bidRequest, context) { + const imp = buildImp(bidRequest, context); + + if (bidRequest?.params?.adUnitId) { + imp.tagid = bidRequest.params.adUnitId; + } + + const floor = getBidFloor(bidRequest); + if (floor) { + imp.bidfloor = floor; + imp.bidfloorcur = 'USD'; + } + + return imp; + }, + request(buildRequest, imps, bidderRequest, context) { + return buildRequest(imps, bidderRequest, context); + }, + bidResponse(buildBidResponse, bid, context) { + const isValidBidType = Object.keys(context.bidRequest.mediaTypes).includes(getMediaType(bid)); + + if (isValidBidType) { + return buildBidResponse(bid, context); + } + + logError('Incorrect bid type for bid: ', bid.id); + }, + context: { + netRevenue: true, + ttl: 30 + }, + translator: toOrtb26 +}); + +function getBidFloor(bid) { + if (isFn(bid.getFloor)) { + const floor = bid.getFloor({ + currency: 'USD', + mediaType: '*', + size: '*' + }); + if (isPlainObject(floor) && !isNaN(floor.floor) && floor.currency === 'USD') { + return floor.floor; + } + } + return bid.params?.floor; +} + +function isValidBidFloorCurrency(bid) { + return !bid.ortb2Imp?.bidfloorcur || bid.ortb2Imp.bidfloorcur === 'USD'; +} + +function isValidParams(bid) { + const tenantId = bid?.params?.tenantId; + const adUnitId = bid?.params?.adUnitId; + return typeof tenantId === 'string' && tenantId.length > 0 && + (adUnitId == null || (typeof adUnitId === 'string' && adUnitId.length > 0)); +} + +function makeUrl(bidRequests) { + return `${ENDPOINT_URL}?${formatQS({ pid: bidRequests[0].params.tenantId })}`; +} + +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: [VIDEO, BANNER], + isBidRequestValid: (bid) => !!bid && isValidParams(bid) && isValidBidFloorCurrency(bid), + + buildRequests: (bidRequests, bidderRequest) => { + const data = converter.toORTB({ bidRequests, bidderRequest }); + + return [{ + method: METHOD, + url: makeUrl(bidRequests), + options: { + contentType: 'application/json', + withCredentials: true, + crossOrigin: true + }, + data: data, + }]; + }, + + interpretResponse: ({ body }, req) => { + if (!body || !body.seatbid || body.seatbid.length === 0) { + return []; + } + return converter.fromORTB({ + response: body, + request: req.data + }); + }, + + onTimeout: (data) => { }, + + onBidWon: (bid) => { + if (bid.nurl) { + const url = new URL(bid.nurl); + url.searchParams.set('cpm', bid.cpm); + fetch(url.toString(), { method: 'GET', keepalive: true }).catch(err => + logError('Error triggering win notification', err) + ); + } + }, + + onSetTargeting: (bid) => { }, + + getUserSyncs: function(syncOptions, serverResponses, gdprConsent, uspConsent, gppConsent) { + const syncs = []; + + if (serverResponses && (syncOptions.iframeEnabled || syncOptions.pixelEnabled)) { + const params = formatQS(getUserSyncParams(gdprConsent, uspConsent, gppConsent)); + + if (syncOptions.iframeEnabled) { + serverResponses.forEach(response => { + const iframeUrl = response?.body?.ext?.[BIDDER_CODE]?.sync?.iframe; + if (iframeUrl) { + syncs.push({ + type: "iframe", + url: `${iframeUrl}${params ? `${iframeUrl.lastIndexOf('?') !== -1 ? '&' : '?'}${params}` : ''}`, + }); + } + }); + } else if (syncOptions.pixelEnabled) { + serverResponses.forEach(response => { + const images = response?.body?.ext?.[BIDDER_CODE]?.sync?.image || []; + images.forEach(image => { + syncs.push({ + type: "image", + url: `${image}${params ? `${image.lastIndexOf('?') !== -1 ? '&' : '?'}${params}` : ''}`, + }); + }); + }); + } + } + + return syncs; + } +}; + +registerBidder(spec); diff --git a/modules/synapseHxBidAdapter.md b/modules/synapseHxBidAdapter.md new file mode 100644 index 00000000000..326eebdb4c4 --- /dev/null +++ b/modules/synapseHxBidAdapter.md @@ -0,0 +1,76 @@ +# Overview + +``` +Module Name: Synapse HX Bidder Adapter +Module Type: Bidder Adapter +Maintainer: prebid@compas-inc.com +``` + +# Description + +The Synapse HX Bidder Adapter enables publishers to integrate with Synapse HX exchange for banner and video ad formats. The adapter supports OpenRTB standards and processes bid requests efficiently using the Prebid.js framework. + +# Test Parameters + +## Sample Banner Ad Unit +``` +var adUnits = [ + { + code: 'test-div', + mediaTypes: { + banner: { + sizes: [[300,250]] + } + }, + bids: [ + { + bidder: 'synapsehx', + params: { + // REQUIRED - Synapse HX tenant identifier + tenantId: 'your-account-id', + // OPTIONAL - Synapse HX ad unit identifier + adUnitId: 'ad-unit-id' + } + } + ] + } +]; +``` + +## Sample Video Ad Unit +``` +var videoAdUnits = [ + { + code: 'test-div-video', + mediaTypes: { + video: { + context: 'instream', + placement: 1, + playerSize: [640, 360], + mimes: ['video/mp4'], + protocols: [2, 3, 5, 6], + api: [2], + maxduration: 30, + linearity: 1, + playbackmethod: [2] + } + }, + bids: [ + { + bidder: 'synapsehx', + params: { + // REQUIRED - Synapse HX tenant identifier + tenantId: 'your-account-id', + // OPTIONAL - Synapse HX ad unit identifier + adUnitId: 'ad-unit-id' + } + } + ] + } +]; +``` + +# Additional Notes +- The adapter processes requests via OpenRTB 2.6 standards. +- Ensure that the `tenantId` parameter is set correctly for your integration. +- The `adUnitId` parameter is optional. diff --git a/test/spec/modules/synapseHxBidAdapter_spec.js b/test/spec/modules/synapseHxBidAdapter_spec.js new file mode 100644 index 00000000000..1163a235b89 --- /dev/null +++ b/test/spec/modules/synapseHxBidAdapter_spec.js @@ -0,0 +1,485 @@ +import { expect } from 'chai'; +import { spec } from 'modules/synapseHxBidAdapter.js'; +import { BANNER, VIDEO } from 'src/mediaTypes.js'; +import { deepClone } from 'src/utils.js'; + +const VALID_PARAMS = { + tenantId: 'tenant_id', + adUnitId: 'ad_unit_id' +}; + +const DEFAULT_REQUEST_DATA = { + adUnitCode: 'test-div', + auctionId: 'b06c5141-fe8f-4cdf-9d7d-54415490a917', + bidId: '32d4d86b4f22ed', + bidder: 'synapsehx', + bidderRequestId: '1bbb7854dfa0d8', + mediaTypes: { + banner: { + sizes: [ + [300, 250], + [300, 600] + ] + } + }, + params: VALID_PARAMS, + src: 'client', + transactionId: 'db739693-9b4a-4669-9945-8eab938783cc' +}; + +const VALID_MEDIA_TYPES_REQUESTS = { + [BANNER]: [{ + ...DEFAULT_REQUEST_DATA, + mediaTypes: { + [BANNER]: { + sizes: [ + [300, 250], + [300, 600] + ] + } + }, + }], + [VIDEO]: [{ + ...DEFAULT_REQUEST_DATA, + mediaTypes: { + [VIDEO]: { + minduration: 3, + maxduration: 43, + playerSize: [640, 480], + mimes: ['video/mp4'], + protocols: [2] + } + }, + }] +}; + +const DEFAULT_BIDDER_REQUEST = { + refererInfo: { referer: 'https://example.com' }, +}; + +const VALID_BIDDER_REQUEST = { + auctionId: '19c97f22-5bd1-4b16-a128-80f75fb0a8a0', + bidderCode: 'synapsehx', + bidderRequestId: '1bbb7854dfa0d8', + bids: [ + { + params: VALID_PARAMS, + } + ], + refererInfo: { + page: 'test-page', + domain: 'test-domain', + ref: 'test-referer' + }, +}; + +const DEFAULT_BID_RESPONSE_DATA = { + 'id': '29596384-e502-4d3c-a47d-4f16b16bd554', + 'impid': '32d4d86b4f22ed', + 'price': 0.18417903447819028, + 'adid': '2:64:162:1001', + 'adomain': [ + 'synapsehx' + ], + 'nurl': 'https://synapsehx/v1', + 'lurl': 'https://synapsehx/v1', + 'iurl': 'https://synapsehx/v1', + 'cid': '1982494692188097775', + 'crid': '5889732975267688811', + 'cat': ['IAB1-1', 'IAB1-6'], + 'w': 300, + 'h': 250, +}; + +const EXT_SYNC = { + synapsehx: { + sync: { + image: [ + 'https://sync.hx.compasonline.com/pbserver/image/1', + 'https://sync.hx.compasonline.com/pbserver/image/2', + 'https://sync.hx.compasonline.com/pbserver/image/3', + ], + iframe: 'https://sync.hx.compasonline.com/pbserver/iframe' + } + } +}; + +const SERVER_RESPONSE_BANNER = { + 'id': '5d997535-e900-4a6b-9cb7-737e402d5cfa', + 'seatbid': [ + { + 'bid': [ + { + ...DEFAULT_BID_RESPONSE_DATA, + 'mtype': 1, + 'adm': 'banner.img', + 'ext': { + 'duration': 0, + 'prebid': { + 'type': BANNER + } + } + } + ], + 'seat': 'synapsehx', + 'group': 0 + } + ], + 'cur': 'USD', + 'ext': EXT_SYNC +}; + +const SERVER_RESPONSE_VIDEO = { + 'id': '8cd85aed-25a6-4db0-ad98-4a3af1f7601c', + 'seatbid': [ + { + 'bid': [ + { + ...DEFAULT_BID_RESPONSE_DATA, + 'mtype': 2, + 'adm': '', + 'ext': { + 'duration': 0, + 'prebid': { + 'type': VIDEO + } + } + } + ], + 'seat': 'synapsehx', + 'group': 0 + } + ], + 'cur': 'USD', + 'ext': EXT_SYNC +}; + +describe('Prebid Adapter: Synapse HX', function () { + describe('code returns a bidder code of synapsehx', function () { + expect(spec.code).to.eql('synapsehx'); + }); + + describe('isBidRequestValid', function () { + it('verifies bidder params', function () { + const validTenantAndAdUnit = { + bidder: 'synapsehx', + params: VALID_PARAMS + }; + expect(spec.isBidRequestValid(validTenantAndAdUnit)).to.eql(true); + + const invalidAdUnit = { + bidder: 'synapsehx', + params: { + tenantId: 'tenant_id', + adUnitId: 1 + } + }; + expect(spec.isBidRequestValid(invalidAdUnit)).to.eql(false); + + const emptyAdUnit = { + bidder: 'synapsehx', + params: { + tenantId: 'tenant_id', + adUnitId: '' + } + }; + expect(spec.isBidRequestValid(emptyAdUnit)).to.eql(false); + + const noAdUnit = { + bidder: 'synapsehx', + params: { + tenantId: 'tenant_id' + } + }; + expect(spec.isBidRequestValid(noAdUnit)).to.eql(true); + + const invalidTenant = { + bidder: 'synapsehx', + params: { + tenantId: 1 + } + }; + expect(spec.isBidRequestValid(invalidTenant)).to.eql(false); + + const emptyTenant = { + bidder: 'synapsehx', + params: { + tenantId: '' + } + }; + expect(spec.isBidRequestValid(emptyTenant)).to.eql(false); + }); + it('verifies bidFloorCur', function () { + const bidRequestUSD = { + bidder: 'synapsehx', + params: VALID_PARAMS, + ortb2Imp: { + bidfloorcur: 'USD' + } + }; + expect(spec.isBidRequestValid(bidRequestUSD)).to.eql(true); + + const bidRequestEUR = { + bidder: 'synapsehx', + params: VALID_PARAMS, + ortb2Imp: { + bidfloorcur: 'EUR' + } + }; + expect(spec.isBidRequestValid(bidRequestEUR)).to.eql(false); + }); + }); + + describe('buildRequests', function () { + it('should build request for banner media type', function () { + const bidRequest = VALID_MEDIA_TYPES_REQUESTS[BANNER][0]; + + const requests = spec.buildRequests([bidRequest], DEFAULT_BIDDER_REQUEST); + + expect(requests).to.have.lengthOf(1); + const request = requests[0]; + expect(request.method).to.equal('POST'); + expect(request.url).to.contain('pid=tenant_id'); + expect(request.data).to.have.property('imp'); + }); + + it('should provide bidfloor when either bid param or getFloor function exists', function () { + let bidRequest = deepClone(DEFAULT_REQUEST_DATA); + + // with no param or getFloor bidfloor is not specified + let request = spec.buildRequests([bidRequest], DEFAULT_BIDDER_REQUEST)[0].data; + expect(request.imp[0].bidfloor).to.not.exist; + expect(request.imp[0].bidfloorcur).to.not.exist; + + // with param and no getFloor bidfloor uses value from param + bidRequest.params.floor = 1.3; + request = spec.buildRequests([bidRequest], DEFAULT_BIDDER_REQUEST)[0].data; + expect(request.imp[0].bidfloor).to.equal(1.3); + expect(request.imp[0].bidfloorcur).to.equal('USD'); + + // with param and getFloor bidfloor uses value form getFloor + bidRequest.getFloor = () => { return { currency: 'USD', floor: 2.4 }; }; + request = spec.buildRequests([bidRequest], DEFAULT_BIDDER_REQUEST)[0].data; + expect(request.imp[0].bidfloor).to.equal(2.4); + expect(request.imp[0].bidfloorcur).to.equal('USD'); + }); + + it('should provide coppa', () => { + let bidderRequest = deepClone(DEFAULT_BIDDER_REQUEST); + bidderRequest.ortb2 = { regs: { coppa: 0 } }; + let request = spec.buildRequests([DEFAULT_REQUEST_DATA], bidderRequest)[0].data; + expect(request.regs.coppa).to.equal(0); + + bidderRequest.ortb2 = { regs: { coppa: 1 } }; + request = spec.buildRequests([DEFAULT_REQUEST_DATA], bidderRequest)[0].data; + expect(request.regs.coppa).to.equal(1); + }); + + it('should provide GPP', () => { + let bidderRequest = deepClone(DEFAULT_BIDDER_REQUEST); + bidderRequest.ortb2 = { regs: { gpp: 0, gpp_sid: [] } }; + let request = spec.buildRequests([DEFAULT_REQUEST_DATA], bidderRequest)[0].data; + expect(request.regs.gpp).to.equal(0); + expect(request.regs.gpp_sid).to.deep.equal([]); + + bidderRequest.ortb2 = { regs: { gpp: 1, gpp_sid: [1, 2] } }; + request = spec.buildRequests([DEFAULT_REQUEST_DATA], bidderRequest)[0].data; + expect(request.regs.gpp).to.equal(1); + expect(request.regs.gpp_sid).to.deep.equal([1, 2]); + }); + + it('should provide GDPR', () => { + let bidderRequest = deepClone(DEFAULT_BIDDER_REQUEST); + bidderRequest.ortb2 = { regs: { gdpr: 0 }, user: { consent: '' } }; + let request = spec.buildRequests([DEFAULT_REQUEST_DATA], bidderRequest)[0].data; + expect(request.regs.gdpr).to.equal(0); + expect(request.user.consent).to.equal(''); + + bidderRequest.ortb2 = { regs: { gdpr: 1 }, user: { consent: 'GDPR_string' } }; + request = spec.buildRequests([DEFAULT_REQUEST_DATA], bidderRequest)[0].data; + expect(request.regs.gdpr).to.equal(1); + expect(request.user.consent).to.equal('GDPR_string'); + }); + + it('should provide us_privacy', function () { + let bidderRequest = deepClone(DEFAULT_BIDDER_REQUEST); + + bidderRequest.ortb2 = { regs: { us_privacy: '1YYN' } }; + const request = spec.buildRequests([DEFAULT_REQUEST_DATA], bidderRequest)[0].data; + + expect(request.regs.us_privacy).to.equal('1YYN'); + }); + + if (FEATURES.VIDEO) { + it('should build request for video media type', function () { + const bidRequest = VALID_MEDIA_TYPES_REQUESTS[VIDEO][0]; + + const requests = spec.buildRequests([bidRequest], DEFAULT_BIDDER_REQUEST); + + expect(requests).to.have.lengthOf(1); + const request = requests[0]; + + expect(request.data.imp[0].video).to.exist; + expect(request.data.imp[0].video.minduration).to.equal(3); + expect(request.data.imp[0].video.maxduration).to.equal(43); + }); + } + }); + + describe('interpretResponse', function () { + it('should return a valid bid array with a banner bid', () => { + const requests = spec.buildRequests(VALID_MEDIA_TYPES_REQUESTS[BANNER], VALID_BIDDER_REQUEST); + const { data } = requests[0]; + const bids = spec.interpretResponse({ body: SERVER_RESPONSE_BANNER }, { data }).bids; + + expect(bids).to.be.a('array').that.has.lengthOf(1); + bids.forEach(value => { + expect(value).to.be.a('object').that.has.all.keys( + 'ad', 'cpm', 'creativeId', 'currency', 'height', 'mediaType', 'meta', 'netRevenue', 'requestId', 'ttl', 'width', 'seatBidId', 'creative_id' + ); + }); + }); + + it('should set meta.adomain from the bid response adomain field', () => { + const requests = spec.buildRequests(VALID_MEDIA_TYPES_REQUESTS[BANNER], VALID_BIDDER_REQUEST); + const { data } = requests[0]; + const bids = spec.interpretResponse({ body: SERVER_RESPONSE_BANNER }, { data }).bids; + + expect(bids).to.have.lengthOf(1); + const bid = bids[0]; + + expect(bid.meta).to.be.an('object'); + expect(bid.meta.advertiserDomains).to.be.an('array').that.includes('synapsehx'); + }); + + if (FEATURES.VIDEO) { + it('should return a valid bid array with a video bid', () => { + const requests = spec.buildRequests(VALID_MEDIA_TYPES_REQUESTS[VIDEO], VALID_BIDDER_REQUEST); + const { data } = requests[0]; + const bids = spec.interpretResponse({ body: SERVER_RESPONSE_VIDEO }, { data }).bids; + expect(bids).to.be.a('array').that.has.lengthOf(1); + bids.forEach(value => { + expect(value).to.be.a('object').that.has.all.keys( + 'vastUrl', 'vastXml', 'playerHeight', 'playerWidth', 'cpm', 'creativeId', 'currency', 'height', 'mediaType', 'meta', 'netRevenue', 'requestId', 'ttl', 'width', 'seatBidId', 'creative_id' + ); + }); + }); + } + }); + + describe('getUserSyncs', function () { + it('pixelEnabled and iframeEnabled on', function () { + const syncOptions = { pixelEnabled: true, iframeEnabled: true }; + const syncs = spec.getUserSyncs(syncOptions, [{ body: SERVER_RESPONSE_BANNER }]); + expect(syncs).to.have.length(1); + expect(syncs[0].type).to.equal('iframe'); + expect(syncs[0].url).to.equal('https://sync.hx.compasonline.com/pbserver/iframe'); + }); + + it('pixelEnabled on', function () { + const syncOptions = { pixelEnabled: true, iframeEnabled: false }; + const syncs = spec.getUserSyncs(syncOptions, [{ body: SERVER_RESPONSE_BANNER }]); + expect(syncs).to.have.length(3); + expect(syncs).to.deep.equal([ + { type: 'image', url: 'https://sync.hx.compasonline.com/pbserver/image/1' }, + { type: 'image', url: 'https://sync.hx.compasonline.com/pbserver/image/2' }, + { type: 'image', url: 'https://sync.hx.compasonline.com/pbserver/image/3' } + ]); + }); + + it('iframeEnabled on', function () { + const syncOptions = { pixelEnabled: false, iframeEnabled: true }; + const syncs = spec.getUserSyncs(syncOptions, [{ body: SERVER_RESPONSE_BANNER }]); + expect(syncs).to.have.length(1); + expect(syncs[0].type).to.equal('iframe'); + expect(syncs[0].url).to.equal('https://sync.hx.compasonline.com/pbserver/iframe'); + }); + + it('pixelEnabled and iframeEnabled off', function () { + const syncOptions = { pixelEnabled: false, iframeEnabled: false }; + const syncs = spec.getUserSyncs(syncOptions, [{ body: SERVER_RESPONSE_BANNER }]); + expect(syncs).to.have.length(0); + }); + + [{ iframeEnabled: true }, { pixelEnabled: true }].forEach(option => { + // eslint-disable-next-line no-console + console.log(`requests: '${JSON.stringify(option)}'`); + describe(`privacy parameters with ${JSON.stringify(option)}`, function () { + it('with GDPR applies = false', function () { + const gdprConsent = { + gdprApplies: false, + consentString: '' + }; + + const syncs = spec.getUserSyncs(option, [{ body: SERVER_RESPONSE_BANNER }], gdprConsent); + + expect(syncs).to.not.be.empty; + syncs.forEach(sync => { + expect(sync.url).to.include('gdpr=0'); + }); + }); + + it('with GDPR applies = true', function () { + const gdprConsent = { + gdprApplies: true, + consentString: 'GDPR_string' + }; + + const syncs = spec.getUserSyncs(option, [{ body: SERVER_RESPONSE_BANNER }], gdprConsent); + + expect(syncs).to.not.be.empty; + syncs.forEach(sync => { + expect(sync.url).to.include('gdpr=1'); + expect(sync.url).to.include('gdpr_consent=GDPR_string'); + }); + }); + + it('with us_privacy', function () { + const syncs = spec.getUserSyncs(option, [{ body: SERVER_RESPONSE_BANNER }], undefined, '1YNN'); + + expect(syncs).to.not.be.empty; + syncs.forEach(sync => { + expect(sync.url).to.include('us_privacy=1YNN'); + }); + }); + + it('with GPP', function () { + const gppConsent = { + gppString: 'GPP_string', + applicableSections: [1, 2] + }; + + const syncs = spec.getUserSyncs(option, [{ body: SERVER_RESPONSE_BANNER }], undefined, undefined, gppConsent); + + expect(syncs).to.not.be.empty; + syncs.forEach(sync => { + expect(sync.url).to.include('gpp=GPP_string'); + expect(sync.url).to.include('gpp_sid=1,2'); + }); + }); + + it('with all consent params', function () { + const gdprConsent = { + gdprApplies: true, + consentString: 'GDPR_string' + }; + const gppConsent = { + gppString: 'GPP_string', + applicableSections: [1, 2] + }; + + const syncs = spec.getUserSyncs(option, [{ body: SERVER_RESPONSE_BANNER }], gdprConsent, '1YNN', gppConsent); + + expect(syncs).to.not.be.empty; + syncs.forEach(sync => { + expect(sync.url).to.include('gdpr=1'); + expect(sync.url).to.include('gdpr_consent=GDPR_string'); + expect(sync.url).to.include('us_privacy=1YNN'); + expect(sync.url).to.include('gpp=GPP_string'); + expect(sync.url).to.include('gpp_sid=1,2'); + }); + }); + }); + }); + }); +}); From c4cb58fe95a652a41d536ee7e926ad27e115d98a Mon Sep 17 00:00:00 2001 From: prebid-compas Date: Mon, 27 Jul 2026 03:02:49 +0300 Subject: [PATCH 2/3] Adjustments according to review --- modules/synapseHxBidAdapter.js | 10 +++++----- test/spec/modules/synapseHxBidAdapter_spec.js | 20 ++++++++++++++++--- 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/modules/synapseHxBidAdapter.js b/modules/synapseHxBidAdapter.js index 2f382ff6740..1e305f336b1 100644 --- a/modules/synapseHxBidAdapter.js +++ b/modules/synapseHxBidAdapter.js @@ -48,8 +48,7 @@ const converter = ortbConverter({ context: { netRevenue: true, ttl: 30 - }, - translator: toOrtb26 + } }); function getBidFloor(bid) { @@ -87,13 +86,13 @@ export const spec = { isBidRequestValid: (bid) => !!bid && isValidParams(bid) && isValidBidFloorCurrency(bid), buildRequests: (bidRequests, bidderRequest) => { - const data = converter.toORTB({ bidRequests, bidderRequest }); + const data = toOrtb26(converter.toORTB({ bidRequests, bidderRequest })); return [{ method: METHOD, url: makeUrl(bidRequests), options: { - contentType: 'application/json', + contentType: 'text/plain', withCredentials: true, crossOrigin: true }, @@ -105,10 +104,11 @@ export const spec = { if (!body || !body.seatbid || body.seatbid.length === 0) { return []; } + body.cur ??= "USD"; return converter.fromORTB({ response: body, request: req.data - }); + }).bids; }, onTimeout: (data) => { }, diff --git a/test/spec/modules/synapseHxBidAdapter_spec.js b/test/spec/modules/synapseHxBidAdapter_spec.js index 1163a235b89..9e95d21cbbb 100644 --- a/test/spec/modules/synapseHxBidAdapter_spec.js +++ b/test/spec/modules/synapseHxBidAdapter_spec.js @@ -330,7 +330,7 @@ describe('Prebid Adapter: Synapse HX', function () { it('should return a valid bid array with a banner bid', () => { const requests = spec.buildRequests(VALID_MEDIA_TYPES_REQUESTS[BANNER], VALID_BIDDER_REQUEST); const { data } = requests[0]; - const bids = spec.interpretResponse({ body: SERVER_RESPONSE_BANNER }, { data }).bids; + const bids = spec.interpretResponse({ body: SERVER_RESPONSE_BANNER }, { data }); expect(bids).to.be.a('array').that.has.lengthOf(1); bids.forEach(value => { @@ -340,10 +340,24 @@ describe('Prebid Adapter: Synapse HX', function () { }); }); + it('should use USD if cur not specified', () => { + const requests = spec.buildRequests(VALID_MEDIA_TYPES_REQUESTS[BANNER], VALID_BIDDER_REQUEST); + const { data } = requests[0]; + const bidRespomseNoCur = { ...SERVER_RESPONSE_BANNER }; + delete bidRespomseNoCur.cur; + const bids = spec.interpretResponse({ body: bidRespomseNoCur }, { data }); + + expect(bids).to.be.a('array').that.has.lengthOf(1); + bids.forEach(value => { + expect(value.currency).to.exist; + expect(value.currency).to.equal('USD'); + }); + }); + it('should set meta.adomain from the bid response adomain field', () => { const requests = spec.buildRequests(VALID_MEDIA_TYPES_REQUESTS[BANNER], VALID_BIDDER_REQUEST); const { data } = requests[0]; - const bids = spec.interpretResponse({ body: SERVER_RESPONSE_BANNER }, { data }).bids; + const bids = spec.interpretResponse({ body: SERVER_RESPONSE_BANNER }, { data }); expect(bids).to.have.lengthOf(1); const bid = bids[0]; @@ -356,7 +370,7 @@ describe('Prebid Adapter: Synapse HX', function () { it('should return a valid bid array with a video bid', () => { const requests = spec.buildRequests(VALID_MEDIA_TYPES_REQUESTS[VIDEO], VALID_BIDDER_REQUEST); const { data } = requests[0]; - const bids = spec.interpretResponse({ body: SERVER_RESPONSE_VIDEO }, { data }).bids; + const bids = spec.interpretResponse({ body: SERVER_RESPONSE_VIDEO }, { data }); expect(bids).to.be.a('array').that.has.lengthOf(1); bids.forEach(value => { expect(value).to.be.a('object').that.has.all.keys( From 5fddad7c3710e48ef850cbafd1242619ac2d1e83 Mon Sep 17 00:00:00 2001 From: prebid-compas Date: Thu, 30 Jul 2026 12:08:35 +0300 Subject: [PATCH 3/3] Adjustments according to review # 2 --- ...HxBidAdapter.js => synapsehxBidAdapter.js} | 33 ++++--------- ...HxBidAdapter.md => synapsehxBidAdapter.md} | 0 ...er_spec.js => synapsehxBidAdapter_spec.js} | 47 +++++++++++++++++-- 3 files changed, 50 insertions(+), 30 deletions(-) rename modules/{synapseHxBidAdapter.js => synapsehxBidAdapter.js} (82%) rename modules/{synapseHxBidAdapter.md => synapsehxBidAdapter.md} (100%) rename test/spec/modules/{synapseHxBidAdapter_spec.js => synapsehxBidAdapter_spec.js} (91%) diff --git a/modules/synapseHxBidAdapter.js b/modules/synapsehxBidAdapter.js similarity index 82% rename from modules/synapseHxBidAdapter.js rename to modules/synapsehxBidAdapter.js index 1e305f336b1..c4073c28656 100644 --- a/modules/synapseHxBidAdapter.js +++ b/modules/synapsehxBidAdapter.js @@ -1,6 +1,7 @@ import { registerBidder } from '../src/adapters/bidderFactory.js'; import { BANNER, VIDEO } from '../src/mediaTypes.js'; -import { logError, isFn, isPlainObject, formatQS } from '../src/utils.js'; +import { logError, formatQS, triggerPixel } from '../src/utils.js'; +import { getBidFloor } from '../libraries/adrelevantisUtils/bidderUtils.js'; import { ortbConverter } from '../libraries/ortbConverter/converter.js'; import { toOrtb26 } from '../libraries/ortb2.5Translator/translator.js'; import { getUserSyncParams } from '../libraries/userSyncUtils/userSyncUtils.js'; @@ -10,11 +11,8 @@ const METHOD = 'POST'; const ENDPOINT_URL = `https://rtb.hx.compasonline.com/pbjs`; function getMediaType(bid) { - if (bid.mtype) { - const mtypeToMediaType = { 1: BANNER, 2: VIDEO }; - return mtypeToMediaType[bid.mtype]; - } - return bid?.ext?.prebid?.type; + const mtypeToMediaType = { 1: BANNER, 2: VIDEO }; + return mtypeToMediaType[bid.mtype]; } const converter = ortbConverter({ @@ -25,7 +23,7 @@ const converter = ortbConverter({ imp.tagid = bidRequest.params.adUnitId; } - const floor = getBidFloor(bidRequest); + const floor = getBidFloor(bidRequest, 'USD'); if (floor) { imp.bidfloor = floor; imp.bidfloorcur = 'USD'; @@ -51,22 +49,9 @@ const converter = ortbConverter({ } }); -function getBidFloor(bid) { - if (isFn(bid.getFloor)) { - const floor = bid.getFloor({ - currency: 'USD', - mediaType: '*', - size: '*' - }); - if (isPlainObject(floor) && !isNaN(floor.floor) && floor.currency === 'USD') { - return floor.floor; - } - } - return bid.params?.floor; -} - function isValidBidFloorCurrency(bid) { - return !bid.ortb2Imp?.bidfloorcur || bid.ortb2Imp.bidfloorcur === 'USD'; + const currency = bid.ortb2Imp?.bidfloorcur; + return currency == null || currency === 'USD'; } function isValidParams(bid) { @@ -117,9 +102,7 @@ export const spec = { if (bid.nurl) { const url = new URL(bid.nurl); url.searchParams.set('cpm', bid.cpm); - fetch(url.toString(), { method: 'GET', keepalive: true }).catch(err => - logError('Error triggering win notification', err) - ); + triggerPixel(url.toString()); } }, diff --git a/modules/synapseHxBidAdapter.md b/modules/synapsehxBidAdapter.md similarity index 100% rename from modules/synapseHxBidAdapter.md rename to modules/synapsehxBidAdapter.md diff --git a/test/spec/modules/synapseHxBidAdapter_spec.js b/test/spec/modules/synapsehxBidAdapter_spec.js similarity index 91% rename from test/spec/modules/synapseHxBidAdapter_spec.js rename to test/spec/modules/synapsehxBidAdapter_spec.js index 9e95d21cbbb..e48072602f5 100644 --- a/test/spec/modules/synapseHxBidAdapter_spec.js +++ b/test/spec/modules/synapsehxBidAdapter_spec.js @@ -1,7 +1,8 @@ import { expect } from 'chai'; -import { spec } from 'modules/synapseHxBidAdapter.js'; +import { spec } from 'modules/synapsehxBidAdapter.js'; import { BANNER, VIDEO } from 'src/mediaTypes.js'; import { deepClone } from 'src/utils.js'; +import * as utils from 'src/utils.js'; const VALID_PARAMS = { tenantId: 'tenant_id', @@ -252,7 +253,7 @@ describe('Prebid Adapter: Synapse HX', function () { expect(request.imp[0].bidfloorcur).to.not.exist; // with param and no getFloor bidfloor uses value from param - bidRequest.params.floor = 1.3; + bidRequest.params.reserve = 1.3; request = spec.buildRequests([bidRequest], DEFAULT_BIDDER_REQUEST)[0].data; expect(request.imp[0].bidfloor).to.equal(1.3); expect(request.imp[0].bidfloorcur).to.equal('USD'); @@ -343,9 +344,9 @@ describe('Prebid Adapter: Synapse HX', function () { it('should use USD if cur not specified', () => { const requests = spec.buildRequests(VALID_MEDIA_TYPES_REQUESTS[BANNER], VALID_BIDDER_REQUEST); const { data } = requests[0]; - const bidRespomseNoCur = { ...SERVER_RESPONSE_BANNER }; - delete bidRespomseNoCur.cur; - const bids = spec.interpretResponse({ body: bidRespomseNoCur }, { data }); + const bidResponseNoCur = { ...SERVER_RESPONSE_BANNER }; + delete bidResponseNoCur.cur; + const bids = spec.interpretResponse({ body: bidResponseNoCur }, { data }); expect(bids).to.be.a('array').that.has.lengthOf(1); bids.forEach(value => { @@ -354,6 +355,22 @@ describe('Prebid Adapter: Synapse HX', function () { }); }); + it('should not convert bid if media type does not match', () => { + const requests = spec.buildRequests(VALID_MEDIA_TYPES_REQUESTS[BANNER], VALID_BIDDER_REQUEST); + const { data } = requests[0]; + const bids = spec.interpretResponse({ body: SERVER_RESPONSE_VIDEO }, { data }); + + expect(bids).to.deep.equal([]); + }); + + it('should return empty array if no bids', () => { + const requests = spec.buildRequests(VALID_MEDIA_TYPES_REQUESTS[BANNER], VALID_BIDDER_REQUEST); + const { data } = requests[0]; + const bids = spec.interpretResponse({ body: null }, { data }); + + expect(bids).to.deep.equal([]); + }); + it('should set meta.adomain from the bid response adomain field', () => { const requests = spec.buildRequests(VALID_MEDIA_TYPES_REQUESTS[BANNER], VALID_BIDDER_REQUEST); const { data } = requests[0]; @@ -496,4 +513,24 @@ describe('Prebid Adapter: Synapse HX', function () { }); }); }); + + describe('onBidWon', function() { + let triggerPixelStub; + + beforeEach(function() { + triggerPixelStub = sinon.stub(utils, 'triggerPixel').resolves({ ok: true }); + }); + afterEach(function() { + sinon.restore(); + }); + + it('Should trigger pixel', function() { + spec.onBidWon({ nurl: 'http://example.com/win', 'cpm': 2.13 }); + + expect(triggerPixelStub.callCount).to.equal(1); + + const [url] = triggerPixelStub.firstCall.args; + expect(url).to.equal('http://example.com/win?cpm=2.13'); + }); + }); });