diff --git a/modules/condorxBidAdapter.js b/modules/condorxBidAdapter.js index d40595b3b21..92f16c96e25 100644 --- a/modules/condorxBidAdapter.js +++ b/modules/condorxBidAdapter.js @@ -2,14 +2,57 @@ import { BANNER, NATIVE } from '../src/mediaTypes.js'; import { createTrackPixelHtml, inIframe } from '../src/utils.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { convertOrtbRequestToProprietaryNative } from '../src/native.js'; +import { ortbConverter } from '../libraries/ortbConverter/converter.js'; const BIDDER_CODE = 'condorx'; const API_URL = 'https://api.condorx.io/cxb/get.json'; +const ORTB_API_BASE = 'https://api.condorx.io/cxb'; const REQUEST_METHOD = 'GET'; -const MAX_SIZE_DEVIATION = 0.05; -const SUPPORTED_AD_SIZES = [ - [100, 100], [200, 200], [300, 250], [336, 280], [400, 200], [300, 200], [600, 600], [236, 202], [1080, 1920], [300, 374] -]; +const ORTB_REQUEST_METHOD = 'POST'; + +const converter = ortbConverter({ + context: { + netRevenue: true, + ttl: 360 + }, + imp(buildImp, bidRequest, context) { + const imp = buildImp(bidRequest, context); + + // Add CondorX specific extensions + imp.ext = { + widget: bidRequest.params.widget, + website: bidRequest.params.website, + ...imp.ext + }; + + return imp; + }, + request(buildRequest, imps, bidderRequest, context) { + const request = buildRequest(imps, bidderRequest, context); + + // Add CondorX specific extensions + request.ext = { + website: bidderRequest.bids[0].params.website, + widget: bidderRequest.bids[0].params.widget, + ...request.ext + }; + + return request; + }, + bidResponse(buildBidResponse, bid, context) { + const bidResponse = buildBidResponse(bid, context); + + // Handle CondorX specific response format + if (bid.ext && bid.ext.condorx) { + bidResponse.meta = { + ...bidResponse.meta, + advertiserDomains: bid.ext.condorx.domain ? [bid.ext.condorx.domain] : [] + }; + } + + return bidResponse; + } +}); function getBidRequestUrl(bidRequest, bidderRequest) { if (bidRequest.params.url && bidRequest.params.url !== 'current url') { @@ -57,6 +100,9 @@ function parseNativeAdResponse(tile, response) { } function parseBannerAdResponse(tile, response) { + if (tile.adm) { + return tile.adm; + } if (tile.tag) { return tile.tag; } @@ -91,17 +137,38 @@ function getBidderAdSizes(bidRequest) { } function isValidAdSize([width, height]) { - if (!width || !height) { - return false; + return width > 0 && height > 0; +} + +function getAdSize(bidRequest) { + const validSize = getValidAdSize(bidRequest); + return validSize || [300, 250]; // Default fallback size +} + +function getBidFloor(bidRequest) { + if (bidRequest.params && bidRequest.params.bidfloor && !isNaN(bidRequest.params.bidfloor)) { + return parseFloat(bidRequest.params.bidfloor); } - return SUPPORTED_AD_SIZES.some(([supportedWidth, supportedHeight]) => { - if (supportedWidth === width && supportedHeight === height) { - return true; + if (typeof bidRequest.getFloor === 'function') { + try { + const floorInfo = bidRequest.getFloor({ + currency: 'USD', + mediaType: bidRequest.nativeParams ? 'native' : 'banner', + size: getAdSize(bidRequest) || [300, 250] + }); + return floorInfo.floor || -1; + } catch (e) { + return -1; } - const supportedRatio = supportedWidth / supportedHeight; - const ratioDeviation = supportedRatio / width * height; - return Math.abs(ratioDeviation - 1) <= MAX_SIZE_DEVIATION && (supportedWidth > width || (width - supportedWidth) / width <= MAX_SIZE_DEVIATION); - }); + } + + return -1; +} + +function getOpenRTBEndpoint(bidRequest) { + const websiteWidget = `${bidRequest.params.website}_${bidRequest.params.widget}`; + const base64WebsiteWidget = btoa(websiteWidget); + return `${ORTB_API_BASE}/${base64WebsiteWidget}/openrtb.json`; } export const bidderSpec = { @@ -119,11 +186,32 @@ export const bidderSpec = { }, buildRequests: function (validBidRequests, bidderRequest) { + const useOpenRTB = validBidRequests[0].params.useOpenRTB === true; + + if (useOpenRTB) { + // Use Prebid's ORTB converter + const ortbRequest = converter.toORTB({ + bidderRequest, + bidRequests: validBidRequests + }); + + return [{ + url: getOpenRTBEndpoint(validBidRequests[0]), + method: ORTB_REQUEST_METHOD, + data: ortbRequest, + bids: validBidRequests, + options: {}, + ortbRequest // Store for response processing + }]; + } + + // Legacy format validBidRequests = convertOrtbRequestToProprietaryNative(validBidRequests); - if (!validBidRequests) { + if (!validBidRequests || !validBidRequests.length) { return []; } + return validBidRequests.map(bidRequest => { if (bidRequest.params) { const mediaType = bidRequest.hasOwnProperty('nativeParams') ? 1 : 2; @@ -131,6 +219,8 @@ export const bidderSpec = { const widgetId = bidRequest.params.widget; const websiteId = bidRequest.params.website; const pageUrl = getBidRequestUrl(bidRequest, bidderRequest); + const bidFloor = getBidFloor(bidRequest); + let subid; try { let url @@ -144,7 +234,8 @@ export const bidderSpec = { subid = widgetId; } const bidId = bidRequest.bidId; - let apiUrl = `${API_URL}?w=${websiteId}&wg=${widgetId}&u=${pageUrl}&s=${subid}&p=0&ireqid=${bidId}&prebid=${mediaType}&imgw=${imageWidth}&imgh=${imageHeight}`; + let apiUrl = `${API_URL}?w=${websiteId}&wg=${widgetId}&u=${pageUrl}&s=${subid}&p=0&ireqid=${bidId}&prebid=${mediaType}&imgw=${imageWidth}&imgh=${imageHeight}&bf=${bidFloor}`; + if (bidderRequest && bidderRequest.gdprConsent && bidderRequest.gdprApplies && bidderRequest.consentString) { apiUrl += `&g=1&gc=${bidderRequest.consentString}`; } @@ -158,6 +249,15 @@ export const bidderSpec = { }, interpretResponse: function (serverResponse, bidRequest) { + if (bidRequest.ortbRequest) { + const response = converter.fromORTB({ + request: bidRequest.ortbRequest, + response: serverResponse.body + }); + return response.bids; + } + + // Legacy format response if (!serverResponse.body || !serverResponse.body.tiles || !serverResponse.body.tiles.length) { return []; } diff --git a/modules/condorxBidAdapter.md b/modules/condorxBidAdapter.md index 2df8be7220a..d8d748dc75a 100644 --- a/modules/condorxBidAdapter.md +++ b/modules/condorxBidAdapter.md @@ -8,7 +8,7 @@ Maintainer: prebid@condorx.io # Description -Module that connects to CondorX bidder to fetch bids. +Module that connects to CondorX bidder to fetch bids. Supports both legacy and OpenRTB request formats. # Test Parameters ``` @@ -24,7 +24,8 @@ Module that connects to CondorX bidder to fetch bids. params: { widget: 'widget id by CondorX', website: 'website id by CondorX', - url:'current url' + url:'current url', + bidfloor: 0.50 } }] }, @@ -56,9 +57,61 @@ Module that connects to CondorX bidder to fetch bids. params: { widget: 'widget id by CondorX', website: 'website id by CondorX', - url:'current url' + url:'current url', + bidfloor: 0.75 + } + }] + }, + { + code: 'condorx-container-id', + mediaTypes: { + banner: { + sizes: [[728, 90]], + } + }, + bids: [{ + bidder: "condorx", + params: { + widget: 'widget id by CondorX', + website: 'website id by CondorX', + url:'current url', + bidfloor: 1.00, + useOpenRTB: true } }] } }]; ``` + +# Parameters + +| Name | Scope | Description | Example | Type | Default | +|------|-------|-------------|---------|------|---------| +| widget | required | Widget ID provided by CondorX | `123456` | integer | - | +| website | required | Website ID provided by CondorX | `789012` | integer | - | +| url | optional | Page URL override | `'https://example.com'` | string | `'current url'` | +| bidfloor | optional | Minimum bid price in USD | `0.50` | number | `-1` | +| useOpenRTB | optional | Enable OpenRTB format requests | `true` | boolean | `false` | + +# Request Formats + +## Legacy Format (Default) +Uses GET request to legacy endpoint with query parameters: +``` +GET https://api.condorx.io/cxb/get.json?w=789012&wg=123456&... +``` + +## OpenRTB Format +Uses POST request to OpenRTB endpoint with JSON payload: +``` +POST https://api.condorx.io/cxb/openrtb.json +Content-Type: application/json + +{ + "id": "auction-id", + "imp": [...], + "site": {...} +} +``` + +To enable OpenRTB format, set `useOpenRTB: true` in the bid parameters. diff --git a/test/spec/modules/condorxBidAdapter_spec.js b/test/spec/modules/condorxBidAdapter_spec.js index 337e0dfeb33..62dfb631bd2 100644 --- a/test/spec/modules/condorxBidAdapter_spec.js +++ b/test/spec/modules/condorxBidAdapter_spec.js @@ -5,6 +5,7 @@ import * as utils from 'src/utils.js'; describe('CondorX Bid Adapter Tests', function () { let basicBidRequests; let nativeBidData; + let ortbBidRequests; const defaultRequestParams = { widget: 274572, website: 195491, @@ -15,7 +16,9 @@ describe('CondorX Bid Adapter Tests', function () { basicBidRequests = [ { bidder: 'condorx', - params: defaultRequestParams + params: defaultRequestParams, + bidId: 'test-bid-id-1', + sizes: [[300, 250]] } ]; @@ -23,6 +26,8 @@ describe('CondorX Bid Adapter Tests', function () { { bidder: 'condorx', params: defaultRequestParams, + bidId: 'test-bid-id-2', + sizes: [[100, 100]], nativeParams: { title: { required: true, @@ -38,6 +43,18 @@ describe('CondorX Bid Adapter Tests', function () { } } ]; + + ortbBidRequests = [ + { + bidder: 'condorx', + params: { + ...defaultRequestParams, + useOpenRTB: true + }, + bidId: 'test-bid-id-3', + sizes: [[300, 250]] + } + ]; }); describe('Bid Size Validation', function () { @@ -63,6 +80,24 @@ describe('CondorX Bid Adapter Tests', function () { const isValid = adapterSpec.isBidRequestValid(bid); expect(isValid).to.be.true; }); + + it('should accept any valid size', function () { + bid.sizes = [[728, 90]]; + const isValid = adapterSpec.isBidRequestValid(bid); + expect(isValid).to.be.true; + }); + + it('should reject invalid size', function () { + bid.sizes = [[0, 0]]; + const isValid = adapterSpec.isBidRequestValid(bid); + expect(isValid).to.be.false; + }); + + it('should reject negative size', function () { + bid.sizes = [[-1, -1]]; + const isValid = adapterSpec.isBidRequestValid(bid); + expect(isValid).to.be.false; + }); }); describe('Bid Request Validation', function () { @@ -76,6 +111,19 @@ describe('CondorX Bid Adapter Tests', function () { expect(isValid).to.be.true; }); + it('should validate a correct OpenRTB bid request', function () { + const validOrtbBid = { + bidder: 'condorx', + params: { + ...defaultRequestParams, + useOpenRTB: true + }, + sizes: [[300, 250]] + }; + const isValid = adapterSpec.isBidRequestValid(validOrtbBid); + expect(isValid).to.be.true; + }); + it('should invalidate an empty params bid request', function () { const invalidBid = { bidder: 'condorx', @@ -89,7 +137,7 @@ describe('CondorX Bid Adapter Tests', function () { const invalidBid = { bidder: 'condorx', params: { - widget: '55765a', // Invalid value for widget + widget: '55765a', website: 195491, url: 'current url' }, @@ -100,6 +148,236 @@ describe('CondorX Bid Adapter Tests', function () { }); }); + describe('OpenRTB Format Support', function () { + let bidderRequest; + + beforeEach(function () { + bidderRequest = { + auctionId: 'test-auction-id', + bids: ortbBidRequests, + ortb2: { + site: { + page: 'https://condorx.io' + } + }, + refererInfo: { + page: 'https://condorx.io' + } + }; + }); + + it('should build OpenRTB request when useOpenRTB is true', function () { + const request = adapterSpec.buildRequests(ortbBidRequests, bidderRequest)[0]; + expect(request.method).to.equal('POST'); + expect(request.url).to.include('/openrtb.json'); + expect(request.data).to.be.an('object'); + }); + + it('should build correct OpenRTB endpoint URL', function () { + const request = adapterSpec.buildRequests(ortbBidRequests, bidderRequest)[0]; + const expectedBase64 = btoa('195491_274572'); + expect(request.url).to.equal(`https://api.condorx.io/cxb/${expectedBase64}/openrtb.json`); + }); + + it('should include ortbRequest in request object', function () { + const request = adapterSpec.buildRequests(ortbBidRequests, bidderRequest)[0]; + expect(request.ortbRequest).to.exist; + expect(request.ortbRequest).to.be.an('object'); + }); + + it('should parse JSON data in OpenRTB request', function () { + const request = adapterSpec.buildRequests(ortbBidRequests, bidderRequest)[0]; + const ortbData = request.data; + expect(ortbData).to.have.property('id'); + expect(ortbData).to.have.property('imp'); + expect(ortbData.imp).to.be.an('array'); + expect(ortbData.imp[0]).to.have.property('ext'); + expect(ortbData.imp[0].ext).to.have.property('widget', 274572); + expect(ortbData.imp[0].ext).to.have.property('website', 195491); + }); + + it('should use legacy format when useOpenRTB is false', function () { + const request = adapterSpec.buildRequests(basicBidRequests, bidderRequest)[0]; + expect(request.method).to.equal('GET'); + expect(request.url).to.include('https://api.condorx.io/cxb/get.json'); + expect(request.data).to.equal(''); + }); + + it('should return OpenRTB request format when useOpenRTB is true', function () { + const request = adapterSpec.buildRequests(ortbBidRequests, bidderRequest)[0]; + expect(request.method).to.equal('POST'); + expect(request.url).to.include('/openrtb.json'); + expect(request.data).to.be.an('object'); + expect(request.ortbRequest).to.exist; + }); + }); + + describe('First Party Data (ORTB2)', function () { + let bidderRequest; + + beforeEach(function () { + bidderRequest = { + auctionId: 'test-auction-id', + bids: ortbBidRequests, + ortb2: { + site: { + page: 'https://condorx.io' + } + }, + refererInfo: { + page: 'https://condorx.io' + } + }; + }); + + it('should pass user data in OpenRTB request', function () { + bidderRequest.ortb2.user = { + id: 'user123', + buyeruid: 'buyer456', + yob: 1990 + }; + + const request = adapterSpec.buildRequests(ortbBidRequests, bidderRequest)[0]; + const ortbData = request.data; + + expect(ortbData.user.id).to.equal('user123'); + expect(ortbData.user.buyeruid).to.equal('buyer456'); + expect(ortbData.user.yob).to.equal(1990); + }); + + it('should pass device data in OpenRTB request', function () { + bidderRequest.ortb2.device = { + ua: 'Mozilla/5.0 Test Browser', + language: 'en-US', + devicetype: 1, + make: 'Apple', + model: 'iPhone' + }; + + const request = adapterSpec.buildRequests(ortbBidRequests, bidderRequest)[0]; + const ortbData = request.data; + + expect(ortbData.device.ua).to.equal('Mozilla/5.0 Test Browser'); + expect(ortbData.device.language).to.equal('en-US'); + expect(ortbData.device.devicetype).to.equal(1); + expect(ortbData.device.make).to.equal('Apple'); + expect(ortbData.device.model).to.equal('iPhone'); + }); + + it('should pass site data in OpenRTB request', function () { + bidderRequest.ortb2.site = { + page: 'https://condorx.io/test-page', + domain: 'condorx.io', + cat: ['IAB1'], + keywords: 'test,keywords' + }; + + const request = adapterSpec.buildRequests(ortbBidRequests, bidderRequest)[0]; + const ortbData = request.data; + + expect(ortbData.site.page).to.equal('https://condorx.io/test-page'); + expect(ortbData.site.domain).to.equal('condorx.io'); + expect(ortbData.site.cat).to.deep.equal(['IAB1']); + expect(ortbData.site.keywords).to.equal('test,keywords'); + }); + + it('should pass custom extensions in OpenRTB request', function () { + bidderRequest.ortb2.ext = { + pageType: 'article', + customData: 'test-value' + }; + + const request = adapterSpec.buildRequests(ortbBidRequests, bidderRequest)[0]; + const ortbData = request.data; + + expect(ortbData.ext.pageType).to.equal('article'); + expect(ortbData.ext.customData).to.equal('test-value'); + }); + + it('should pass blocked categories and domains', function () { + bidderRequest.ortb2.bcat = ['IAB25', 'IAB26']; + bidderRequest.ortb2.badv = ['blocked-advertiser.com']; + + const request = adapterSpec.buildRequests(ortbBidRequests, bidderRequest)[0]; + const ortbData = request.data; + + expect(ortbData.bcat).to.deep.equal(['IAB25', 'IAB26']); + expect(ortbData.badv).to.deep.equal(['blocked-advertiser.com']); + }); + }); + + describe('Bid Floor Support', function () { + it('should include bid floor in request URL when provided in params', function () { + const bidWithFloor = { + bidder: 'condorx', + params: { + ...defaultRequestParams, + bidfloor: 0.75 + }, + sizes: [[300, 250]] + }; + const request = adapterSpec.buildRequests([bidWithFloor])[0]; + const urlParams = new URL(request.url).searchParams; + expect(urlParams.get('bf')).to.equal('0.75'); + }); + + it('should include -1 bid floor when no floor is provided', function () { + const bidWithoutFloor = { + bidder: 'condorx', + params: defaultRequestParams, + sizes: [[300, 250]] + }; + const request = adapterSpec.buildRequests([bidWithoutFloor])[0]; + const urlParams = new URL(request.url).searchParams; + expect(urlParams.get('bf')).to.equal('-1'); + }); + + it('should prioritize params.bidfloor over getFloor function', function () { + const bidWithBothFloors = { + bidder: 'condorx', + params: { + ...defaultRequestParams, + bidfloor: 0.5 + }, + sizes: [[300, 250]], + getFloor: function() { + return { floor: 1.25 }; + } + }; + const request = adapterSpec.buildRequests([bidWithBothFloors])[0]; + const urlParams = new URL(request.url).searchParams; + expect(urlParams.get('bf')).to.equal('0.5'); + }); + + it('should handle getFloor function errors gracefully', function () { + const bidWithErrorFloor = { + bidder: 'condorx', + params: defaultRequestParams, + sizes: [[300, 250]], + getFloor: function() { + throw new Error('Floor error'); + } + }; + const request = adapterSpec.buildRequests([bidWithErrorFloor])[0]; + const urlParams = new URL(request.url).searchParams; + expect(urlParams.get('bf')).to.equal('-1'); + }); + + it('should handle invalid bidfloor params', function () { + const bidWithInvalidFloor = { + bidder: 'condorx', + params: { + ...defaultRequestParams, + bidfloor: 'invalid' + }, + sizes: [[300, 250]] + }; + const request = adapterSpec.buildRequests([bidWithInvalidFloor])[0]; + const urlParams = new URL(request.url).searchParams; + expect(urlParams.get('bf')).to.equal('-1'); + }); + }); + describe('Request Building and HTTP Calls', function () { it('should verify the API HTTP method', function () { const request = adapterSpec.buildRequests(basicBidRequests)[0]; @@ -127,7 +405,7 @@ describe('CondorX Bid Adapter Tests', function () { }); it('should validate the custom URL parameter', function () { - const customUrl = 'https://i.am.url'; + const customUrl = 'https://condorx.io/custom-page'; basicBidRequests[0].params.url = customUrl; const request = adapterSpec.buildRequests(basicBidRequests)[0]; const urlParams = new URL(request.url).searchParams; @@ -138,6 +416,7 @@ describe('CondorX Bid Adapter Tests', function () { describe('Response Validation', function () { let nativeResponseData; let bannerResponseData; + let ortbResponseData; beforeEach(() => { const baseResponse = { @@ -175,6 +454,23 @@ describe('CondorX Bid Adapter Tests', function () { config: '{"css":".__condorx_banner_title{display:block!important;}"}' }, }; + + ortbResponseData = { + id: 'response-123', + seatbid: [{ + bid: [{ + id: 'bid-123', + impid: 'condorx121212', + price: 0.5, + adm: '
Test Banner Ad
', + w: 300, + h: 250, + crid: '12345', + adomain: ['condorx.test'] + }] + }], + cur: 'USD' + }; }); it('should return an empty array for missing response', function () {