From 8769789e2647bf2fbc6196cf2502789d651e9905 Mon Sep 17 00:00:00 2001 From: Milica Grujicic Date: Fri, 8 May 2026 15:01:30 +0200 Subject: [PATCH 1/4] Add video support to cwire bidder --- modules/cwireBidAdapter.js | 75 ++++++++++-- modules/cwireBidAdapter.md | 32 +++++ test/spec/modules/cwireBidAdapter_spec.js | 141 ++++++++++++++++++++++ 3 files changed, 240 insertions(+), 8 deletions(-) diff --git a/modules/cwireBidAdapter.js b/modules/cwireBidAdapter.js index 46647b31c58..695929a12fc 100644 --- a/modules/cwireBidAdapter.js +++ b/modules/cwireBidAdapter.js @@ -1,6 +1,6 @@ import { registerBidder } from "../src/adapters/bidderFactory.js"; import { getStorageManager } from "../src/storageManager.js"; -import { BANNER } from "../src/mediaTypes.js"; +import { BANNER, VIDEO } from "../src/mediaTypes.js"; import { getParameterByName, isNumber, @@ -22,6 +22,7 @@ import { getAdUnitElement } from '../src/utils/adUnits.js'; // ------------------------------------ const BIDDER_CODE = "cwire"; const CWID_KEY = "cw_cwid"; +const VAST_XML_REGEX = /^\s*(?:<\?xml[^>]*>\s*)?]/i; export const BID_ENDPOINT = "https://prebid.cwi.re/v1/bid"; export const EVENT_ENDPOINT = "https://prebid.cwi.re/v1/event"; @@ -169,10 +170,68 @@ function getCwExtension() { }; } +function getBidRequest(responseBid, request) { + return request?.bids?.find((bid) => ( + bid.bidId === responseBid.requestId || + bid.bidId === responseBid.bidId + )); +} + +function isVastXml(value) { + return typeof value === "string" && VAST_XML_REGEX.test(value); +} + +function isVideoResponse(responseBid, requestBid) { + const hasVast = ( + responseBid.vastXml || + responseBid.vastXML || + responseBid.vastUrl || + responseBid.vastURL || + isVastXml(responseBid.html) + ); + const hasVideoMediaType = ( + responseBid.mediaType === VIDEO || + responseBid.adType === VIDEO || + responseBid.type === VIDEO + ); + const requestSupportsVideo = !!requestBid?.mediaTypes?.video; + + return ( + hasVast || + (requestSupportsVideo && (hasVideoMediaType || !requestBid?.mediaTypes?.banner)) + ); +} + +function mapBidResponse(responseBid, request) { + const { html, vastXml, vastXML, vast, vastUrl, vastURL, ...rest } = responseBid; + const requestBid = getBidRequest(responseBid, request); + + if (isVideoResponse(responseBid, requestBid)) { + const vastXmlResponse = vastXml || vastXML || vast || (isVastXml(html) ? html : null); + const vastUrlResponse = vastUrl || vastURL; + + if (!vastXmlResponse && !vastUrlResponse) { + return null; + } + + return { + ...rest, + mediaType: VIDEO, + ...(vastXmlResponse && { vastXml: vastXmlResponse }), + ...(vastUrlResponse && { vastUrl: vastUrlResponse }), + }; + } + + return { + ...rest, + ad: html, + }; +} + export const spec = { code: BIDDER_CODE, gvlid: GVL_ID, - supportedMediaTypes: [BANNER], + supportedMediaTypes: [BANNER, VIDEO], /** * Determines whether the given bid request is valid. @@ -255,6 +314,7 @@ export const spec = { method: "POST", url: BID_ENDPOINT, data: payloadString, + bids: validBidRequests, }; }, /** @@ -271,12 +331,11 @@ export const spec = { } } - // Rename `html` response property to `ad` as used by prebid. - const bids = serverResponse.body?.bids.map(({ html, ...rest }) => ({ - ...rest, - ad: html, - })); - return bids || []; + // Rename banner `html` to `ad`; video responses must expose VAST. + const bids = (serverResponse.body?.bids || []) + .map((bid) => mapBidResponse(bid, bidRequest)) + .filter(Boolean); + return bids; }, onBidWon: function (bid) { diff --git a/modules/cwireBidAdapter.md b/modules/cwireBidAdapter.md index 1d4f3c039c8..90e49db42ca 100644 --- a/modules/cwireBidAdapter.md +++ b/modules/cwireBidAdapter.md @@ -27,6 +27,8 @@ Below, the list of C-WIRE params and where they can be set. ### adUnit configuration +#### Banner + ```javascript var adUnits = [ { @@ -65,6 +67,36 @@ var adUnits = [ ]; ``` +#### Video + +```javascript +var adUnits = [ + { + code: 'video_target_div_id', // REQUIRED + mediaTypes: { + video: { + context: 'instream', + playerSize: [640, 480], + mimes: ['video/mp4'], + protocols: [2, 3, 5, 6], + startdelay: 0, + placement: 1, + playbackmethod: [2], + api: [2], + linearity: 1 + } + }, + bids: [{ + bidder: 'cwire', + params: { + domainId: 1422, // required - number + placementId: 2211521, // optional - number + } + }] + } +]; +``` + ### URL parameters For debugging and testing purposes url parameters can be set. diff --git a/test/spec/modules/cwireBidAdapter_spec.js b/test/spec/modules/cwireBidAdapter_spec.js index 6943618cf51..2ce256a37dd 100644 --- a/test/spec/modules/cwireBidAdapter_spec.js +++ b/test/spec/modules/cwireBidAdapter_spec.js @@ -7,6 +7,7 @@ import sinon, { stub } from "sinon"; import { config } from "../../../src/config.js"; import * as autoplayLib from "../../../libraries/autoplayDetection/autoplay.js"; import * as adUnits from 'src/utils/adUnits'; +import { BANNER, VIDEO } from "../../../src/mediaTypes.js"; describe("C-WIRE bid adapter", () => { config.setConfig({ debug: true }); @@ -53,6 +54,31 @@ describe("C-WIRE bid adapter", () => { ], }, }; + const vastXml = ''; + const videoBidRequest = { + bidder: "cwire", + params: { + domainId: 4057, + }, + adUnitCode: "video-adunit-code", + mediaTypes: { + video: { + context: "instream", + playerSize: [640, 480], + mimes: ["video/mp4"], + protocols: [2, 3, 5, 6], + startdelay: 0, + placement: 1, + playbackmethod: [2], + api: [2], + linearity: 1, + }, + }, + bidId: "video-bid-id", + bidderRequestId: "video-request-id", + auctionId: "video-auction-id", + transactionId: "video-transaction-id", + }; beforeEach(function () { sandbox = sinon.createSandbox(); @@ -68,6 +94,10 @@ describe("C-WIRE bid adapter", () => { expect(spec.buildRequests).to.exist.and.to.be.a("function"); expect(spec.interpretResponse).to.exist.and.to.be.a("function"); }); + + it("supports banner and video media types", function () { + expect(spec.supportedMediaTypes).to.include.members([BANNER, VIDEO]); + }); }); describe("buildRequests", function () { it("sends bid request to ENDPOINT via POST", function () { @@ -75,6 +105,14 @@ describe("C-WIRE bid adapter", () => { expect(request.url).to.equal(BID_ENDPOINT); expect(request.method).to.equal("POST"); }); + + it("passes video mediaTypes to the bid endpoint", function () { + const request = spec.buildRequests([videoBidRequest], bidderRequest); + const payload = JSON.parse(request.data); + + expect(payload.slots[0].mediaTypes.video).to.deep.equal(videoBidRequest.mediaTypes.video); + expect(request.bids[0]).to.deep.equal(videoBidRequest); + }); }); describe("buildRequests with given creative", function () { let utilsStub; @@ -318,6 +356,109 @@ describe("C-WIRE bid adapter", () => { expect(bids[0].ad).to.exist; }); + + it("keeps banner responses on the banner path even if a custom type field is present", function () { + const bidResponse = deepClone(response); + bidResponse.body.bids[0].type = VIDEO; + const bids = spec.interpretResponse(bidResponse, { bids: bidRequests }); + + expect(bids[0].ad).to.equal("

Hello world

"); + expect(bids[0].mediaType).to.not.equal(VIDEO); + }); + + it("maps VAST XML responses to video bids", function () { + const bidResponse = { + body: { + bids: [ + { + cpm: 5, + currency: "USD", + dimensions: [640, 480], + netRevenue: true, + creativeId: "video-creative", + requestId: videoBidRequest.bidId, + ttl: 360, + vastXml, + }, + ], + }, + }; + const bids = spec.interpretResponse(bidResponse, { bids: [videoBidRequest] }); + + expect(bids[0].mediaType).to.equal(VIDEO); + expect(bids[0].vastXml).to.equal(vastXml); + expect(bids[0].ad).to.not.exist; + }); + + it("maps VAST URL responses to video bids", function () { + const vastUrl = "https://prebid.cwi.re/vast/video-ad.xml"; + const bidResponse = { + body: { + bids: [ + { + cpm: 5, + currency: "USD", + dimensions: [640, 480], + netRevenue: true, + creativeId: "video-creative", + requestId: videoBidRequest.bidId, + ttl: 360, + vastUrl, + }, + ], + }, + }; + const bids = spec.interpretResponse(bidResponse, { bids: [videoBidRequest] }); + + expect(bids[0].mediaType).to.equal(VIDEO); + expect(bids[0].vastUrl).to.equal(vastUrl); + }); + + it("uses VAST XML from html when responding to a video-only request", function () { + const bidResponse = { + body: { + bids: [ + { + html: vastXml, + cpm: 5, + currency: "USD", + dimensions: [640, 480], + netRevenue: true, + creativeId: "video-creative", + requestId: videoBidRequest.bidId, + ttl: 360, + }, + ], + }, + }; + const bids = spec.interpretResponse(bidResponse, { bids: [videoBidRequest] }); + + expect(bids[0].mediaType).to.equal(VIDEO); + expect(bids[0].vastXml).to.equal(vastXml); + expect(bids[0].ad).to.not.exist; + }); + + it("drops video responses that do not include VAST", function () { + const bidResponse = { + body: { + bids: [ + { + html: "

Hello world

", + cpm: 5, + currency: "USD", + dimensions: [640, 480], + netRevenue: true, + creativeId: "video-creative", + requestId: videoBidRequest.bidId, + ttl: 360, + }, + ], + }, + }; + const bids = spec.interpretResponse(bidResponse, { bids: [videoBidRequest] }); + + expect(bids).to.be.empty; + }); }); describe("add user-syncs", function () { From aafc6876eb55278279a70b3251537dd6aba4f480 Mon Sep 17 00:00:00 2001 From: Milica Grujicic Date: Thu, 28 May 2026 12:55:50 +0200 Subject: [PATCH 2/4] Convert to ortb --- modules/cwireBidAdapter.js | 421 +++------- modules/cwireBidAdapter.md | 11 +- test/spec/modules/cwireBidAdapter_spec.js | 917 ++++++++++------------ 3 files changed, 523 insertions(+), 826 deletions(-) diff --git a/modules/cwireBidAdapter.js b/modules/cwireBidAdapter.js index 695929a12fc..f260a232489 100644 --- a/modules/cwireBidAdapter.js +++ b/modules/cwireBidAdapter.js @@ -1,135 +1,34 @@ -import { registerBidder } from "../src/adapters/bidderFactory.js"; -import { getStorageManager } from "../src/storageManager.js"; -import { BANNER, VIDEO } from "../src/mediaTypes.js"; +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { getStorageManager } from '../src/storageManager.js'; +import { BANNER, VIDEO } from '../src/mediaTypes.js'; import { getParameterByName, isNumber, logError, logInfo, -} from "../src/utils.js"; -import { getBoundingClientRect } from "../libraries/boundingClientRect/boundingClientRect.js"; -import { hasPurpose1Consent } from "../src/utils/gdpr.js"; -import { sendBeacon } from "../src/ajax.js"; -import { isAutoplayEnabled } from "../libraries/autoplayDetection/autoplay.js"; +} from '../src/utils.js'; +import { ortbConverter } from '../libraries/ortbConverter/converter.js'; +import { getBoundingClientRect } from '../libraries/boundingClientRect/boundingClientRect.js'; +import { hasPurpose1Consent } from '../src/utils/gdpr.js'; +import { sendBeacon } from '../src/ajax.js'; +import { isAutoplayEnabled } from '../libraries/autoplayDetection/autoplay.js'; import { getAdUnitElement } from '../src/utils/adUnits.js'; -/** - * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest - * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid - * @typedef {import('../src/adapters/bidderFactory.js').ServerResponse} ServerResponse - */ +const BIDDER_CODE = 'cwire'; +const CWID_KEY = 'cw_cwid'; -// ------------------------------------ -const BIDDER_CODE = "cwire"; -const CWID_KEY = "cw_cwid"; -const VAST_XML_REGEX = /^\s*(?:<\?xml[^>]*>\s*)?]/i; - -export const BID_ENDPOINT = "https://prebid.cwi.re/v1/bid"; -export const EVENT_ENDPOINT = "https://prebid.cwi.re/v1/event"; +export const BID_ENDPOINT = 'https://prebid.cwi.re/v2/bid'; +export const EVENT_ENDPOINT = 'https://prebid.cwi.re/v2/event'; export const GVL_ID = 1081; export const storage = getStorageManager({ bidderCode: BIDDER_CODE }); -/** - * Retrieve dimensions and CSS max height/width from a given slot and attach the properties to the bidRequest. - * @param bid - * @returns {*&{cwExt: {dimensions: {width: number, height: number}, style: {maxWidth: number, maxHeight: number}}}} - */ -function slotDimensions(bid) { - const adUnitCode = bid.adUnitCode; - const slotEl = getAdUnitElement(bid); - - if (slotEl) { - logInfo(`Slot element found: ${adUnitCode}`); - const { width: slotW, height: slotH } = getBoundingClientRect(slotEl); - const cssMaxW = slotEl.style?.maxWidth; - const cssMaxH = slotEl.style?.maxHeight; - logInfo(`Slot dimensions (w/h): ${slotW} / ${slotH}`); - logInfo(`Slot Styles (maxW/maxH): ${cssMaxW} / ${cssMaxH}`); - - bid = { - ...bid, - cwExt: { - dimensions: { - width: slotW, - height: slotH, - }, - style: { - ...(cssMaxW && { - maxWidth: cssMaxW, - }), - ...(cssMaxH && { - maxHeight: cssMaxH, - }), - }, - }, - }; - } - return bid; -} - -/** - * Extracts feature flags from a comma-separated url parameter `cwfeatures`. - * - * @returns *[] - */ -function getFeatureFlags() { - const ffParam = getParameterByName("cwfeatures"); - if (ffParam) { - return ffParam.split(","); - } - return []; -} - -function getRefGroups() { - const groups = getParameterByName("cwgroups"); - if (groups) { - return groups.split(","); - } - return []; -} - -function getBidFloor(bid) { - if (typeof bid.getFloor !== "function") { - return {}; - } - - const floor = bid.getFloor({ - currency: "USD", - mediaType: "*", - size: "*", - }); - - return floor; -} - -/** - * Returns the downlink speed of the connection in Mbps or an empty string if not available. - */ -function getConnectionDownLink(nav) { - return nav && nav.connection && nav.connection.downlink >= 0 - ? nav.connection.downlink.toString() - : ""; -} - -/** - * Reads the CWID from local storage. - */ function getCwid() { return storage.localStorageIsEnabled() ? storage.getDataFromLocalStorage(CWID_KEY) : null; } -function hasCwid() { - return ( - storage.localStorageIsEnabled() && storage.getDataFromLocalStorage(CWID_KEY) - ); -} - -/** - * Store the CWID to local storage. - */ function updateCwid(cwid) { if (storage.localStorageIsEnabled()) { storage.setDataInLocalStorage(CWID_KEY, cwid); @@ -138,117 +37,110 @@ function updateCwid(cwid) { } } -/** - * Extract and collect cwire specific extensions. - */ -function getCwExtension() { - const cwId = getCwid(); - const cwCreative = getParameterByName("cwcreative"); - const cwGroups = getRefGroups(); - const cwFeatures = getFeatureFlags(); - // Enable debug flag by passing ?cwdebug=true as url parameter. - // Note: pbjs_debug=true enables it on prebid level - // More info: https://docs.prebid.org/troubleshooting/troubleshooting-guide.html#turn-on-prebidjs-debug-messages - const debug = getParameterByName("cwdebug"); - - return { - ...(cwId && { - cwid: cwId, - }), - ...(cwGroups.length > 0 && { - refgroups: cwGroups, - }), - ...(cwFeatures.length > 0 && { - featureFlags: cwFeatures, - }), - ...(cwCreative && { - cwcreative: cwCreative, - }), - ...(debug && { - debug: true, - }), - }; +function getRefGroups() { + const groups = getParameterByName('cwgroups'); + return groups ? groups.split(',') : []; } -function getBidRequest(responseBid, request) { - return request?.bids?.find((bid) => ( - bid.bidId === responseBid.requestId || - bid.bidId === responseBid.bidId - )); +function getFeatureFlags() { + const ff = getParameterByName('cwfeatures'); + return ff ? ff.split(',') : []; } -function isVastXml(value) { - return typeof value === "string" && VAST_XML_REGEX.test(value); +function getConnectionDownLink(nav) { + return nav?.connection?.downlink >= 0 ? nav.connection.downlink.toString() : ''; } -function isVideoResponse(responseBid, requestBid) { - const hasVast = ( - responseBid.vastXml || - responseBid.vastXML || - responseBid.vastUrl || - responseBid.vastURL || - isVastXml(responseBid.html) - ); - const hasVideoMediaType = ( - responseBid.mediaType === VIDEO || - responseBid.adType === VIDEO || - responseBid.type === VIDEO - ); - const requestSupportsVideo = !!requestBid?.mediaTypes?.video; - - return ( - hasVast || - (requestSupportsVideo && (hasVideoMediaType || !requestBid?.mediaTypes?.banner)) - ); +function getSlotSignals(bidRequest) { + const slotEl = getAdUnitElement(bidRequest); + if (!slotEl) return {}; + const { width, height } = getBoundingClientRect(slotEl); + const maxWidth = slotEl.style?.maxWidth; + const maxHeight = slotEl.style?.maxHeight; + return { + dimensions: { width, height }, + style: { + ...(maxWidth && { maxWidth }), + ...(maxHeight && { maxHeight }), + }, + }; } -function mapBidResponse(responseBid, request) { - const { html, vastXml, vastXML, vast, vastUrl, vastURL, ...rest } = responseBid; - const requestBid = getBidRequest(responseBid, request); - - if (isVideoResponse(responseBid, requestBid)) { - const vastXmlResponse = vastXml || vastXML || vast || (isVastXml(html) ? html : null); - const vastUrlResponse = vastUrl || vastURL; - - if (!vastXmlResponse && !vastUrlResponse) { - return null; +const converter = ortbConverter({ + context: { + netRevenue: true, + ttl: 360, + }, + imp(buildImp, bidRequest, context) { + const imp = buildImp(bidRequest, context); + + const bidderParams = {}; + if (bidRequest.params?.domainId != null) bidderParams.domainId = bidRequest.params.domainId; + if (bidRequest.params?.pageId != null) bidderParams.pageId = bidRequest.params.pageId; + if (bidRequest.params?.placementId != null) bidderParams.placementId = bidRequest.params.placementId; + if (Object.keys(bidderParams).length) { + imp.ext = imp.ext || {}; + imp.ext.bidder = { ...(imp.ext.bidder || {}), ...bidderParams }; } - return { - ...rest, - mediaType: VIDEO, - ...(vastXmlResponse && { vastXml: vastXmlResponse }), - ...(vastUrlResponse && { vastUrl: vastUrlResponse }), + const slotSignals = getSlotSignals(bidRequest); + const cwireExt = { + ...slotSignals, + autoplay: isAutoplayEnabled(), }; - } + imp.ext = imp.ext || {}; + imp.ext.cwire = cwireExt; - return { - ...rest, - ad: html, - }; -} + return imp; + }, + request(buildRequest, imps, bidderRequest, context) { + const request = buildRequest(imps, bidderRequest, context); + + const cwid = getCwid(); + const refgroups = getRefGroups(); + const featureFlags = getFeatureFlags(); + const cwcreative = getParameterByName('cwcreative'); + const debug = getParameterByName('cwdebug'); + + const cwExt = { + ...(cwid && { cwid }), + ...(refgroups.length && { refgroups }), + ...(featureFlags.length && { featureFlags }), + ...(cwcreative && { cwcreative }), + ...(debug && { debug: true }), + pageViewId: bidderRequest.pageViewId, + networkBandwidth: getConnectionDownLink(window.navigator), + sdk: { version: '$prebid.version$' }, + }; + + request.ext = request.ext || {}; + request.ext.cwire = cwExt; + return request; + }, + bidResponse(buildBidResponse, bid, context) { + if (!bid.mtype && context.bidRequest) { + const mt = context.bidRequest.mediaTypes; + if (mt?.video && !mt?.banner) context.mediaType = VIDEO; + else if (mt?.banner && !mt?.video) context.mediaType = BANNER; + } + return buildBidResponse(bid, context); + }, +}); export const spec = { code: BIDDER_CODE, gvlid: GVL_ID, supportedMediaTypes: [BANNER, VIDEO], - /** - * Determines whether the given bid request is valid. - * - * @param {BidRequest} bid The bid params to validate. - * @return boolean True if this is a valid bid, and false otherwise. - */ isBidRequestValid: function (bid) { if (!bid.params?.domainId || !isNumber(bid.params.domainId)) { - logError("domainId not provided or not a number"); + logError('domainId not provided or not a number'); if (!bid.params?.placementId || !isNumber(bid.params.placementId)) { - logError("placementId not provided or not a number"); + logError('placementId not provided or not a number'); return false; } - if (!bid.params?.pageId || !isNumber(bid.params.pageId)) { - logError("pageId not provided or not a number"); + logError('pageId not provided or not a number'); return false; } return true; @@ -256,141 +148,68 @@ export const spec = { return true; }, - /** - * Make a server request from the list of BidRequests. - * - * @param {validBidRequests[]} validBidRequests An array of bids. - * @return ServerRequest Info describing the request to the server. - */ buildRequests: function (validBidRequests, bidderRequest) { - // There are more fields on the refererInfo object - const referrer = bidderRequest?.refererInfo?.page; - - // process bid requests - const processed = validBidRequests - .map((bid) => slotDimensions(bid)) - .map((bid) => { - const bidFloor = getBidFloor(bid); - return { - ...bid, - params: { - ...bid.params, - floor: bidFloor, - }, - }; - }) - .map((bid) => { - const autoplayEnabled = isAutoplayEnabled(); - return { - ...bid, - params: { - ...bid.params, - autoplay: autoplayEnabled, - }, - }; - }) - // Flattens the pageId, domainId and placement Id for backwards compatibility. - .map((bid) => ({ - ...bid, - pageId: bid.params?.pageId, - domainId: bid.params?.domainId, - placementId: bid.params?.placementId, - })); - - const extensions = getCwExtension(); - const payload = { - slots: processed, - httpRef: referrer, - // TODO: Verify whether the auctionId and the usage of pageViewId make sense. - pageViewId: bidderRequest.pageViewId, - networkBandwidth: getConnectionDownLink(window.navigator), - sdk: { - version: "$prebid.version$", - }, - ...extensions, - }; - const payloadString = JSON.stringify(payload); + const data = converter.toORTB({ bidRequests: validBidRequests, bidderRequest }); return { - method: "POST", + method: 'POST', url: BID_ENDPOINT, - data: payloadString, + data, bids: validBidRequests, }; }, - /** - * Unpack the response from the server into a list of bids. - * - * @param {ServerResponse} serverResponse A successful response from the server. - * @return {Bid[]} An array of bids which were nested inside the server. - */ - interpretResponse: function (serverResponse, bidRequest) { - if (!hasCwid()) { - const cwid = serverResponse.body?.cwid; - if (cwid) { - updateCwid(cwid); - } + + interpretResponse: function (serverResponse, request) { + if (!serverResponse?.body) return []; + + const cwid = serverResponse.body?.ext?.cwire?.cwid; + if (cwid && !getCwid()) { + updateCwid(cwid); } - // Rename banner `html` to `ad`; video responses must expose VAST. - const bids = (serverResponse.body?.bids || []) - .map((bid) => mapBidResponse(bid, bidRequest)) - .filter(Boolean); - return bids; + return converter.fromORTB({ + response: serverResponse.body, + request: request.data, + }).bids; }, onBidWon: function (bid) { - logInfo(`Bid won.`); - const event = { - type: "BID_WON", - payload: { - bid: bid, - }, - }; + logInfo('Bid won.'); + const event = { type: 'BID_WON', payload: { bid } }; sendBeacon(EVENT_ENDPOINT, JSON.stringify(event)); }, - onBidderError: function (error, bidderRequest) { + onBidderError: function ({ error, bidderRequest }) { logInfo(`Bidder error: ${error}`); - const event = { - type: "BID_ERROR", - payload: { - error: error, - bidderRequest: bidderRequest, - }, - }; + const event = { type: 'BID_ERROR', payload: { error, bidderRequest } }; sendBeacon(EVENT_ENDPOINT, JSON.stringify(event)); }, - getUserSyncs: function ( - syncOptions, - serverResponses, - gdprConsent, - uspConsent - ) { + getUserSyncs: function (syncOptions, serverResponses, gdprConsent, uspConsent) { logInfo( - "Collecting user-syncs: ", + 'Collecting user-syncs: ', JSON.stringify({ syncOptions, gdprConsent, uspConsent, serverResponses }) ); const syncs = []; if (hasPurpose1Consent(gdprConsent) && gdprConsent.consentString) { - logInfo("GDPR purpose 1 consent was given, adding user-syncs"); + logInfo('GDPR purpose 1 consent was given, adding user-syncs'); const type = syncOptions.pixelEnabled - ? "image" - : null ?? syncOptions.iframeEnabled - ? "iframe" + ? 'image' + : syncOptions.iframeEnabled + ? 'iframe' : null; if (type) { syncs.push({ - type: type, + type, url: `https://ib.adnxs.com/getuid?https://prebid.cwi.re/v1/cookiesync?xandrId=$UID&gdpr=${ gdprConsent.gdprApplies ? 1 : 0 }&gdpr_consent=${gdprConsent.consentString}`, }); } } - logInfo("Collected user-syncs: ", JSON.stringify({ syncs })); + logInfo('Collected user-syncs: ', JSON.stringify({ syncs })); return syncs; }, }; + registerBidder(spec); diff --git a/modules/cwireBidAdapter.md b/modules/cwireBidAdapter.md index 90e49db42ca..bbb7c8979f8 100644 --- a/modules/cwireBidAdapter.md +++ b/modules/cwireBidAdapter.md @@ -8,7 +8,9 @@ Maintainer: devs@cwire.com ## Description -Prebid.js Adapter for C-Wire. +Prebid.js Adapter for C-Wire. Uses native OpenRTB 2.x request/response handling via Prebid's `ortbConverter` library. Supports banner and video. + +Bid requests are POSTed as OpenRTB 2.x JSON to `https://prebid.cwi.re/v2/bid`. Bidder params and cwire-specific signals are carried under `imp[].ext.bidder` and `request.ext.cwire` / `imp[].ext.cwire`. ## Configuration @@ -24,7 +26,6 @@ Below, the list of C-WIRE params and where they can be set. | cwdebug | x | | boolean | NO | | cwfeatures | x | | string | NO | - ### adUnit configuration #### Banner @@ -32,7 +33,7 @@ Below, the list of C-WIRE params and where they can be set. ```javascript var adUnits = [ { - code: 'target_div_id', // REQUIRED + code: 'target_div_id', // REQUIRED bids: [{ bidder: 'cwire', mediaTypes: { @@ -47,7 +48,7 @@ var adUnits = [ }] } ]; -// old version for the compatibility +// legacy configuration (still supported) var adUnits = [ { code: 'target_div_id', // REQUIRED @@ -99,7 +100,7 @@ var adUnits = [ ### URL parameters -For debugging and testing purposes url parameters can be set. +For debugging and testing purposes URL parameters can be set. **Example:** diff --git a/test/spec/modules/cwireBidAdapter_spec.js b/test/spec/modules/cwireBidAdapter_spec.js index 2ce256a37dd..d329b316304 100644 --- a/test/spec/modules/cwireBidAdapter_spec.js +++ b/test/spec/modules/cwireBidAdapter_spec.js @@ -1,84 +1,80 @@ -import { expect } from "chai"; -import { newBidder } from "../../../src/adapters/bidderFactory.js"; -import { BID_ENDPOINT, spec, storage } from "../../../modules/cwireBidAdapter.js"; -import { deepClone, logInfo } from "../../../src/utils.js"; -import * as utils from "src/utils.js"; -import sinon, { stub } from "sinon"; -import { config } from "../../../src/config.js"; -import * as autoplayLib from "../../../libraries/autoplayDetection/autoplay.js"; +import { expect } from 'chai'; +import sinon from 'sinon'; +import { newBidder } from '../../../src/adapters/bidderFactory.js'; +import { + BID_ENDPOINT, + EVENT_ENDPOINT, + spec, + storage, +} from '../../../modules/cwireBidAdapter.js'; +import { deepClone } from '../../../src/utils.js'; +import * as utils from 'src/utils.js'; +import * as ajaxLib from 'src/ajax.js'; +import * as autoplayLib from '../../../libraries/autoplayDetection/autoplay.js'; import * as adUnits from 'src/utils/adUnits'; -import { BANNER, VIDEO } from "../../../src/mediaTypes.js"; +import { BANNER, VIDEO } from '../../../src/mediaTypes.js'; +import 'modules/priceFloors.js'; +import 'modules/currency.js'; -describe("C-WIRE bid adapter", () => { - config.setConfig({ debug: true }); - let sandbox; - const adapter = newBidder(spec); - const bidRequests = [ +function makeBannerBid(overrides = {}) { + return Object.assign( { - bidder: "cwire", - params: { - pageId: "4057", - placementId: "ad-slot-bla", - }, - adUnitCode: "adunit-code", - sizes: [ - [300, 250], - [300, 600], - ], - bidId: "30b31c1838de1e", - bidderRequestId: "22edbae2733bf6", - auctionId: "1d1a030790a475", - transactionId: "04f2659e-c005-4eb1-a57c-fa93145e3843", + bidder: 'cwire', + params: { domainId: 1422, placementId: 2211521 }, + adUnitCode: 'adunit-code', + mediaTypes: { banner: { sizes: [[300, 250], [300, 600]] } }, + bidId: '30b31c1838de1e', + bidderRequestId: '22edbae2733bf6', + auctionId: '1d1a030790a475', + transactionId: '04f2659e-c005-4eb1-a57c-fa93145e3843', }, - ]; - const bidderRequest = { - pageViewId: "326dca71-9ca0-4e8f-9e4d-6106161ac1ad" - } - const response = { - body: { - cwid: "2ef90743-7936-4a82-8acf-e73382a64e94", - hash: "17112D98BBF55D3A", - bids: [ - { - html: "

Hello world

", - cpm: 100, - currency: "CHF", - dimensions: [1, 1], - netRevenue: true, - creativeId: "3454", - requestId: "2c634d4ca5ccfb", - placementId: 177, - transactionId: "b4b32618-1350-4828-b6f0-fbb5c329e9a4", - ttl: 360, + overrides + ); +} + +function makeVideoBid(overrides = {}) { + return Object.assign( + { + bidder: 'cwire', + params: { domainId: 1422 }, + adUnitCode: 'video-adunit', + mediaTypes: { + video: { + context: 'instream', + playerSize: [640, 480], + mimes: ['video/mp4'], + protocols: [2, 3, 5, 6], + api: [2], + linearity: 1, }, - ], - }, - }; - const vastXml = ''; - const videoBidRequest = { - bidder: "cwire", - params: { - domainId: 4057, - }, - adUnitCode: "video-adunit-code", - mediaTypes: { - video: { - context: "instream", - playerSize: [640, 480], - mimes: ["video/mp4"], - protocols: [2, 3, 5, 6], - startdelay: 0, - placement: 1, - playbackmethod: [2], - api: [2], - linearity: 1, }, + bidId: 'video-bid-id', + bidderRequestId: 'video-request-id', + auctionId: 'video-auction-id', + transactionId: 'video-transaction-id', + }, + overrides + ); +} + +function makeBidderRequest(overrides = {}) { + return Object.assign( + { + bidderCode: 'cwire', + auctionId: '1d1a030790a475', + bidderRequestId: '22edbae2733bf6', + pageViewId: '326dca71-9ca0-4e8f-9e4d-6106161ac1ad', + refererInfo: { page: 'https://example.com/article' }, + timeout: 1000, + ortb2: {}, }, - bidId: "video-bid-id", - bidderRequestId: "video-request-id", - auctionId: "video-auction-id", - transactionId: "video-transaction-id", - }; + overrides + ); +} + +describe('C-WIRE bid adapter (ORTB2)', () => { + const adapter = newBidder(spec); + let sandbox; beforeEach(function () { sandbox = sinon.createSandbox(); @@ -87,538 +83,419 @@ describe("C-WIRE bid adapter", () => { afterEach(function () { sandbox.restore(); }); - describe("inherited functions", function () { - it("exists and is a function", function () { - expect(adapter.callBids).to.exist.and.to.be.a("function"); - expect(spec.isBidRequestValid).to.exist.and.to.be.a("function"); - expect(spec.buildRequests).to.exist.and.to.be.a("function"); - expect(spec.interpretResponse).to.exist.and.to.be.a("function"); + + describe('inherited functions', function () { + it('exposes the standard bidderFactory interface', function () { + expect(adapter.callBids).to.exist.and.to.be.a('function'); + expect(spec.isBidRequestValid).to.exist.and.to.be.a('function'); + expect(spec.buildRequests).to.exist.and.to.be.a('function'); + expect(spec.interpretResponse).to.exist.and.to.be.a('function'); }); - it("supports banner and video media types", function () { + it('declares banner and video as supported media types', function () { expect(spec.supportedMediaTypes).to.include.members([BANNER, VIDEO]); }); - }); - describe("buildRequests", function () { - it("sends bid request to ENDPOINT via POST", function () { - const request = spec.buildRequests(bidRequests, bidderRequest); - expect(request.url).to.equal(BID_ENDPOINT); - expect(request.method).to.equal("POST"); - }); - it("passes video mediaTypes to the bid endpoint", function () { - const request = spec.buildRequests([videoBidRequest], bidderRequest); - const payload = JSON.parse(request.data); + it('uses the v2 bid endpoint', function () { + expect(BID_ENDPOINT).to.equal('https://prebid.cwi.re/v2/bid'); + }); - expect(payload.slots[0].mediaTypes.video).to.deep.equal(videoBidRequest.mediaTypes.video); - expect(request.bids[0]).to.deep.equal(videoBidRequest); + it('uses the v2 event endpoint', function () { + expect(EVENT_ENDPOINT).to.equal('https://prebid.cwi.re/v2/event'); }); }); - describe("buildRequests with given creative", function () { - let utilsStub; - beforeEach(function () { - utilsStub = stub(utils, "getParameterByName").callsFake(function () { - return "str-str"; - }); + describe('isBidRequestValid', function () { + it('returns true when domainId is a number', function () { + expect(spec.isBidRequestValid(makeBannerBid({ params: { domainId: 42 } }))).to.equal(true); }); - afterEach(function () { - utilsStub.restore(); + it('returns true with legacy pageId+placementId when domainId is missing', function () { + expect( + spec.isBidRequestValid(makeBannerBid({ params: { pageId: 42, placementId: 99 } })) + ).to.equal(true); }); - it("should add creativeId if url parameter given", function () { - // set from bid.params - const bidRequest = deepClone(bidRequests[0]); + it('returns false when params are absent', function () { + const bid = makeBannerBid(); + delete bid.params; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); - const request = spec.buildRequests([bidRequest], bidderRequest); - const payload = JSON.parse(request.data); - expect(payload.cwcreative).to.exist; - expect(payload.cwcreative).to.deep.equal("str-str"); + it('returns false when domainId is missing and pageId+placementId incomplete', function () { + expect(spec.isBidRequestValid(makeBannerBid({ params: { pageId: 42 } }))).to.equal(false); }); }); - describe("buildRequests reads adUnit offsetWidth and offsetHeight", function () { - beforeEach(function () { - const documentStub = sandbox.stub(adUnits, "getAdUnitElement"); - documentStub.returns({ - offsetWidth: 200, - offsetHeight: 250, - getBoundingClientRect() { - return { width: 200, height: 250 }; - }, - }); + describe('buildRequests: envelope', function () { + it('POSTs a single ORTB2 request to /v2/bid', function () { + const req = spec.buildRequests([makeBannerBid()], makeBidderRequest()); + expect(req.method).to.equal('POST'); + expect(req.url).to.equal(BID_ENDPOINT); + expect(req.data).to.be.an('object'); + expect(req.data.imp).to.be.an('array').with.lengthOf(1); }); - it("width and height should be set", function () { - const bidRequest = deepClone(bidRequests[0]); - const request = spec.buildRequests([bidRequest], bidderRequest); - const payload = JSON.parse(request.data); - - logInfo(JSON.stringify(payload)); - - expect(payload.slots[0].cwExt.dimensions.width).to.equal(200); - expect(payload.slots[0].cwExt.dimensions.height).to.equal(250); - expect(payload.slots[0].cwExt.style.maxHeight).to.not.exist; - expect(payload.slots[0].cwExt.style.maxWidth).to.not.exist; - }); - afterEach(function () { - sandbox.restore(); + it('passes bidderRequest.timeout through as tmax', function () { + const req = spec.buildRequests( + [makeBannerBid()], + makeBidderRequest({ timeout: 750 }) + ); + expect(req.data.tmax).to.equal(750); }); }); - describe("buildRequests reads style attributes", function () { - beforeEach(function () { - const documentStub = sandbox.stub(adUnits, "getAdUnitElement"); - documentStub.returns({ - style: { - maxWidth: "400px", - maxHeight: "350px", - }, - getBoundingClientRect() { - return { width: 0, height: 0 }; - }, - }); - }); - it("css maxWidth should be set", function () { - const bidRequest = deepClone(bidRequests[0]); - - const request = spec.buildRequests([bidRequest], bidderRequest); - const payload = JSON.parse(request.data); - logInfo(JSON.stringify(payload)); - expect(payload.slots[0].cwExt.style.maxWidth).to.eq("400px"); - expect(payload.slots[0].cwExt.style.maxHeight).to.eq("350px"); + describe('buildRequests: imp shape', function () { + it('places bidder params under imp.ext.bidder', function () { + const bid = makeBannerBid({ params: { domainId: 1422, placementId: 2211521 } }); + const req = spec.buildRequests([bid], makeBidderRequest()); + const imp = req.data.imp[0]; + expect(imp.ext.bidder.domainId).to.equal(1422); + expect(imp.ext.bidder.placementId).to.equal(2211521); }); - afterEach(function () { - sandbox.restore(); - }); - }); - describe("buildRequests reads feature flags", function () { - beforeEach(function () { - sandbox.stub(utils, "getParameterByName").callsFake(function () { - return "feature1,feature2"; - }); + it('forwards legacy pageId under imp.ext.bidder when present', function () { + const bid = makeBannerBid({ params: { pageId: 42, placementId: 99 } }); + const req = spec.buildRequests([bid], makeBidderRequest()); + const imp = req.data.imp[0]; + expect(imp.ext.bidder.pageId).to.equal(42); + expect(imp.ext.bidder.placementId).to.equal(99); }); - it("read from url parameter", function () { - const bidRequest = deepClone(bidRequests[0]); - - const request = spec.buildRequests([bidRequest], bidderRequest); - const payload = JSON.parse(request.data); - - logInfo(JSON.stringify(payload)); - - expect(payload.featureFlags).to.exist; - expect(payload.featureFlags).to.include.members(["feature1", "feature2"]); - }); - afterEach(function () { - sandbox.restore(); + it('emits imp.banner when adunit is banner-only', function () { + const req = spec.buildRequests([makeBannerBid()], makeBidderRequest()); + const imp = req.data.imp[0]; + expect(imp.banner).to.exist; + expect(imp.video).to.not.exist; }); - }); - describe("buildRequests reads cwgroups flag", function () { - beforeEach(function () { - sandbox.stub(utils, "getParameterByName").callsFake(function () { - return "group1,group2"; + if (FEATURES.VIDEO) { + it('emits imp.video when adunit is video-only', function () { + const req = spec.buildRequests([makeVideoBid()], makeBidderRequest()); + const imp = req.data.imp[0]; + expect(imp.video).to.exist; + expect(imp.banner).to.not.exist; }); - }); - - it("read from url parameter", function () { - const bidRequest = deepClone(bidRequests[0]); - - const request = spec.buildRequests([bidRequest], bidderRequest); - const payload = JSON.parse(request.data); - logInfo(JSON.stringify(payload)); + it('emits both imp.banner and imp.video for a multi-format adunit', function () { + const bid = makeBannerBid({ + mediaTypes: { + banner: { sizes: [[300, 250]] }, + video: { context: 'instream', playerSize: [640, 480], mimes: ['video/mp4'] }, + }, + }); + const req = spec.buildRequests([bid], makeBidderRequest()); + const imp = req.data.imp[0]; + expect(imp.banner).to.exist; + expect(imp.video).to.exist; + }); + } - expect(payload.refgroups).to.exist; - expect(payload.refgroups).to.include.members(["group1", "group2"]); - }); - afterEach(function () { - sandbox.restore(); + it('populates imp.bidfloor from getFloor when priceFloors module active', function () { + const bid = makeBannerBid({ + getFloor: () => ({ currency: 'USD', floor: 1.23 }), + }); + const req = spec.buildRequests([bid], makeBidderRequest()); + const imp = req.data.imp[0]; + expect(imp.bidfloor).to.equal(1.23); + expect(imp.bidfloorcur).to.equal('USD'); }); }); - describe("buildRequests reads debug flag", function () { - beforeEach(function () { - sandbox.stub(utils, "getParameterByName").callsFake(function () { - return "true"; + describe('buildRequests: imp.ext.cwire (slot signals)', function () { + it('captures slot dimensions when the slot element has bounds', function () { + sandbox.stub(adUnits, 'getAdUnitElement').returns({ + getBoundingClientRect() { + return { width: 200, height: 250 }; + }, + style: {}, }); + const req = spec.buildRequests([makeBannerBid()], makeBidderRequest()); + const ext = req.data.imp[0].ext.cwire; + expect(ext.dimensions).to.deep.equal({ width: 200, height: 250 }); }); - it("read from url parameter", function () { - const bidRequest = deepClone(bidRequests[0]); - - const request = spec.buildRequests([bidRequest], bidderRequest); - const payload = JSON.parse(request.data); - - logInfo(JSON.stringify(payload)); - - expect(payload.debug).to.exist; - expect(payload.debug).to.equal(true); + it('captures maxWidth/maxHeight when slot style sets them', function () { + sandbox.stub(adUnits, 'getAdUnitElement').returns({ + getBoundingClientRect() { + return { width: 0, height: 0 }; + }, + style: { maxWidth: '400px', maxHeight: '350px' }, + }); + const req = spec.buildRequests([makeBannerBid()], makeBidderRequest()); + const ext = req.data.imp[0].ext.cwire; + expect(ext.style.maxWidth).to.equal('400px'); + expect(ext.style.maxHeight).to.equal('350px'); }); - afterEach(function () { - sandbox.restore(); + + it('writes autoplay flag from isAutoplayEnabled', function () { + sandbox.stub(autoplayLib, 'isAutoplayEnabled').returns(true); + const req = spec.buildRequests([makeBannerBid()], makeBidderRequest()); + expect(req.data.imp[0].ext.cwire.autoplay).to.equal(true); }); }); - describe("buildRequests reads cw_id from Localstorage", function () { - before(function () { - sandbox.stub(storage, "localStorageIsEnabled").callsFake(() => true); - sandbox.stub(storage, "setDataInLocalStorage"); - sandbox - .stub(storage, "getDataFromLocalStorage") - .callsFake((key) => "taerfagerg"); + describe('buildRequests: request.ext.cwire (page-level signals)', function () { + it('writes pageViewId and sdk.version', function () { + const req = spec.buildRequests([makeBannerBid()], makeBidderRequest()); + const ext = req.data.ext.cwire; + expect(ext.pageViewId).to.equal('326dca71-9ca0-4e8f-9e4d-6106161ac1ad'); + expect(ext.sdk).to.have.property('version'); }); - it("cw_id is set", function () { - const bidRequest = deepClone(bidRequests[0]); - - const request = spec.buildRequests([bidRequest], bidderRequest); - const payload = JSON.parse(request.data); - - logInfo(JSON.stringify(payload)); - - expect(payload.cwid).to.exist; - expect(payload.cwid).to.equal("taerfagerg"); + it('writes cwid from localStorage when present', function () { + sandbox.stub(storage, 'localStorageIsEnabled').returns(true); + sandbox.stub(storage, 'getDataFromLocalStorage').returns('cwid-from-storage'); + const req = spec.buildRequests([makeBannerBid()], makeBidderRequest()); + expect(req.data.ext.cwire.cwid).to.equal('cwid-from-storage'); }); - afterEach(function () { - sandbox.restore(); + + it('omits cwid when localStorage is disabled', function () { + sandbox.stub(storage, 'localStorageIsEnabled').returns(false); + const req = spec.buildRequests([makeBannerBid()], makeBidderRequest()); + expect(req.data.ext.cwire.cwid).to.equal(undefined); }); - }); - describe("buildRequests maps flattens params for legacy compat", function () { - beforeEach(function () { - const documentStub = sandbox.stub(document, "getElementById"); - documentStub.withArgs(`${bidRequests[0].adUnitCode}`).returns({ - getBoundingClientRect() { - return { width: 0, height: 0 }; - }, + it('writes refgroups from cwgroups URL parameter', function () { + sandbox.stub(utils, 'getParameterByName').callsFake((name) => { + if (name === 'cwgroups') return 'g1,g2'; + return ''; }); + const req = spec.buildRequests([makeBannerBid()], makeBidderRequest()); + expect(req.data.ext.cwire.refgroups).to.deep.equal(['g1', 'g2']); }); - it("pageId flattened", function () { - const bidRequest = deepClone(bidRequests[0]); - const request = spec.buildRequests([bidRequest], bidderRequest); - const payload = JSON.parse(request.data); + it('writes featureFlags from cwfeatures URL parameter', function () { + sandbox.stub(utils, 'getParameterByName').callsFake((name) => { + if (name === 'cwfeatures') return 'f1,f2'; + return ''; + }); + const req = spec.buildRequests([makeBannerBid()], makeBidderRequest()); + expect(req.data.ext.cwire.featureFlags).to.deep.equal(['f1', 'f2']); + }); - logInfo(JSON.stringify(payload)); + it('writes cwcreative from URL parameter', function () { + sandbox.stub(utils, 'getParameterByName').callsFake((name) => { + if (name === 'cwcreative') return '1234'; + return ''; + }); + const req = spec.buildRequests([makeBannerBid()], makeBidderRequest()); + expect(req.data.ext.cwire.cwcreative).to.equal('1234'); + }); - expect(payload.slots[0].pageId).to.exist; + it('writes debug=true when cwdebug URL parameter is set', function () { + sandbox.stub(utils, 'getParameterByName').callsFake((name) => { + if (name === 'cwdebug') return 'true'; + return ''; + }); + const req = spec.buildRequests([makeBannerBid()], makeBidderRequest()); + expect(req.data.ext.cwire.debug).to.equal(true); }); - afterEach(function () { - sandbox.restore(); + + it('omits cwire ext fields that are empty', function () { + sandbox.stub(utils, 'getParameterByName').returns(''); + sandbox.stub(storage, 'localStorageIsEnabled').returns(false); + const req = spec.buildRequests([makeBannerBid()], makeBidderRequest()); + const ext = req.data.ext.cwire; + expect(ext.cwid).to.equal(undefined); + expect(ext.refgroups).to.equal(undefined); + expect(ext.featureFlags).to.equal(undefined); + expect(ext.cwcreative).to.equal(undefined); + expect(ext.debug).to.equal(undefined); }); }); - describe("pageId and placementId are required params", function () { - it("invalid request", function () { - const bidRequest = deepClone(bidRequests[0]); - delete bidRequest.params; - - const valid = spec.isBidRequestValid(bidRequest); - expect(valid).to.be.false; - }); + describe('interpretResponse', function () { + function bannerOrtbResponse(impId) { + return { + seatbid: [ + { + bid: [ + { + id: 'seat-banner', + impid: impId, + price: 1.5, + adm: '

Hello world

', + crid: 'creative-banner', + cid: 'campaign-1', + w: 300, + h: 250, + mtype: 1, + adomain: ['example.com'], + }, + ], + }, + ], + cur: 'USD', + }; + } + + function videoOrtbResponse(impId, vast = '') { + return { + seatbid: [ + { + bid: [ + { + id: 'seat-video', + impid: impId, + price: 5.0, + adm: vast, + crid: 'creative-video', + cid: 'campaign-2', + w: 640, + h: 480, + mtype: 2, + adomain: ['example.com'], + }, + ], + }, + ], + cur: 'USD', + }; + } + + it('maps a banner ORTB response to a Prebid banner bid', function () { + const bid = makeBannerBid(); + const req = spec.buildRequests([bid], makeBidderRequest()); + const bids = spec.interpretResponse({ body: bannerOrtbResponse(req.data.imp[0].id) }, req); + expect(bids).to.have.lengthOf(1); + expect(bids[0].mediaType).to.equal(BANNER); + expect(bids[0].ad).to.equal('

Hello world

'); + expect(bids[0].cpm).to.equal(1.5); + }); + + if (FEATURES.VIDEO) { + it('maps a video ORTB response (mtype=2) to a video bid with vastXml', function () { + const bid = makeVideoBid(); + const req = spec.buildRequests([bid], makeBidderRequest()); + const vast = ''; + const bids = spec.interpretResponse( + { body: videoOrtbResponse(req.data.imp[0].id, vast) }, + req + ); + expect(bids).to.have.lengthOf(1); + expect(bids[0].mediaType).to.equal(VIDEO); + expect(bids[0].vastXml).to.equal(vast); + }); - it("valid request", function () { - const bidRequest = deepClone(bidRequests[0]); - bidRequest.params.pageId = 42; - bidRequest.params.placementId = 42; + it('falls back to mediaType from the request when mtype is missing (video-only adunit)', function () { + const bid = makeVideoBid(); + const req = spec.buildRequests([bid], makeBidderRequest()); + const vast = ''; + const noMtypeResponse = videoOrtbResponse(req.data.imp[0].id, vast); + delete noMtypeResponse.seatbid[0].bid[0].mtype; + const bids = spec.interpretResponse({ body: noMtypeResponse }, req); + expect(bids).to.have.lengthOf(1); + expect(bids[0].mediaType).to.equal(VIDEO); + }); + } - const valid = spec.isBidRequestValid(bidRequest); - expect(valid).to.be.true; + it('returns [] when seatbid is empty', function () { + const bid = makeBannerBid(); + const req = spec.buildRequests([bid], makeBidderRequest()); + const bids = spec.interpretResponse({ body: { seatbid: [], cur: 'USD' } }, req); + expect(bids).to.deep.equal([]); }); - it("cwcreative must be of type string", function () { - const bidRequest = deepClone(bidRequests[0]); - bidRequest.params.pageId = 42; - bidRequest.params.placementId = 42; + it('persists cwid from response.ext.cwire.cwid into localStorage when not already stored', function () { + sandbox.stub(storage, 'localStorageIsEnabled').returns(true); + sandbox.stub(storage, 'getDataFromLocalStorage').returns(null); + const setStub = sandbox.stub(storage, 'setDataInLocalStorage'); + + const bid = makeBannerBid(); + const req = spec.buildRequests([bid], makeBidderRequest()); + const body = bannerOrtbResponse(req.data.imp[0].id); + body.ext = { cwire: { cwid: 'new-cwid-from-server' } }; + spec.interpretResponse({ body }, req); - const valid = spec.isBidRequestValid(bidRequest); - expect(valid).to.be.true; + expect(setStub.calledWith('cw_cwid', 'new-cwid-from-server')).to.equal(true); }); - it("build request adds pageId", function () { - const bidRequest = deepClone(bidRequests[0]); + it('does not overwrite an existing cwid in localStorage', function () { + sandbox.stub(storage, 'localStorageIsEnabled').returns(true); + sandbox.stub(storage, 'getDataFromLocalStorage').returns('existing-cwid'); + const setStub = sandbox.stub(storage, 'setDataInLocalStorage'); - const request = spec.buildRequests([bidRequest], bidderRequest); - const payload = JSON.parse(request.data); + const bid = makeBannerBid(); + const req = spec.buildRequests([bid], makeBidderRequest()); + const body = bannerOrtbResponse(req.data.imp[0].id); + body.ext = { cwire: { cwid: 'new-cwid-from-server' } }; + spec.interpretResponse({ body }, req); - expect(payload.slots[0].pageId).to.exist; + expect(setStub.called).to.equal(false); }); }); - describe("process serverResponse", function () { - it("html to ad mapping", function () { - const bidResponse = deepClone(response); - const bids = spec.interpretResponse(bidResponse, {}); - - expect(bids[0].ad).to.exist; - }); - - it("keeps banner responses on the banner path even if a custom type field is present", function () { - const bidResponse = deepClone(response); - bidResponse.body.bids[0].type = VIDEO; - const bids = spec.interpretResponse(bidResponse, { bids: bidRequests }); - - expect(bids[0].ad).to.equal("

Hello world

"); - expect(bids[0].mediaType).to.not.equal(VIDEO); - }); - - it("maps VAST XML responses to video bids", function () { - const bidResponse = { - body: { - bids: [ - { - cpm: 5, - currency: "USD", - dimensions: [640, 480], - netRevenue: true, - creativeId: "video-creative", - requestId: videoBidRequest.bidId, - ttl: 360, - vastXml, - }, - ], - }, - }; - const bids = spec.interpretResponse(bidResponse, { bids: [videoBidRequest] }); - - expect(bids[0].mediaType).to.equal(VIDEO); - expect(bids[0].vastXml).to.equal(vastXml); - expect(bids[0].ad).to.not.exist; - }); - - it("maps VAST URL responses to video bids", function () { - const vastUrl = "https://prebid.cwi.re/vast/video-ad.xml"; - const bidResponse = { - body: { - bids: [ - { - cpm: 5, - currency: "USD", - dimensions: [640, 480], - netRevenue: true, - creativeId: "video-creative", - requestId: videoBidRequest.bidId, - ttl: 360, - vastUrl, - }, - ], - }, - }; - const bids = spec.interpretResponse(bidResponse, { bids: [videoBidRequest] }); - - expect(bids[0].mediaType).to.equal(VIDEO); - expect(bids[0].vastUrl).to.equal(vastUrl); - }); - - it("uses VAST XML from html when responding to a video-only request", function () { - const bidResponse = { - body: { - bids: [ - { - html: vastXml, - cpm: 5, - currency: "USD", - dimensions: [640, 480], - netRevenue: true, - creativeId: "video-creative", - requestId: videoBidRequest.bidId, - ttl: 360, - }, - ], - }, - }; - const bids = spec.interpretResponse(bidResponse, { bids: [videoBidRequest] }); - - expect(bids[0].mediaType).to.equal(VIDEO); - expect(bids[0].vastXml).to.equal(vastXml); - expect(bids[0].ad).to.not.exist; - }); - - it("drops video responses that do not include VAST", function () { - const bidResponse = { - body: { - bids: [ - { - html: "

Hello world

", - cpm: 5, - currency: "USD", - dimensions: [640, 480], - netRevenue: true, - creativeId: "video-creative", - requestId: videoBidRequest.bidId, - ttl: 360, - }, - ], - }, - }; - const bids = spec.interpretResponse(bidResponse, { bids: [videoBidRequest] }); - - expect(bids).to.be.empty; + describe('event beacons', function () { + it('onBidWon POSTs a BID_WON beacon to the v2 event endpoint', function () { + const sendBeacon = sandbox.stub(ajaxLib, 'sendBeacon'); + const wonBid = { adUnitCode: 'au', cpm: 1.5, requestId: 'r' }; + spec.onBidWon(wonBid); + expect(sendBeacon.calledOnce).to.equal(true); + const [url, payload] = sendBeacon.firstCall.args; + expect(url).to.equal(EVENT_ENDPOINT); + const event = JSON.parse(payload); + expect(event.type).to.equal('BID_WON'); + expect(event.payload.bid).to.deep.equal(wonBid); + }); + + it('onBidderError POSTs a BID_ERROR beacon to the v2 event endpoint', function () { + const sendBeacon = sandbox.stub(ajaxLib, 'sendBeacon'); + const error = { reason: 'boom' }; + const bidderRequest = { bidderCode: 'cwire' }; + spec.onBidderError({ error, bidderRequest }); + expect(sendBeacon.calledOnce).to.equal(true); + const [url, payload] = sendBeacon.firstCall.args; + expect(url).to.equal(EVENT_ENDPOINT); + const event = JSON.parse(payload); + expect(event.type).to.equal('BID_ERROR'); + expect(event.payload.error).to.deep.equal(error); }); }); - describe("add user-syncs", function () { - it("empty user-syncs if no consent given", function () { - const userSyncs = spec.getUserSyncs({}, {}, {}, {}); - - expect(userSyncs).to.be.empty; + describe('getUserSyncs', function () { + it('returns no syncs when GDPR purpose-1 consent is missing', function () { + expect(spec.getUserSyncs({}, {}, {}, {})).to.be.empty; }); - it("empty user-syncs if no syncOption enabled", function () { + + it('returns no syncs when no syncOption is enabled', function () { const gdprConsent = { - vendorData: { - purpose: { - consents: 1, - }, - }, + vendorData: { purpose: { consents: 1 } }, gdprApplies: false, - consentString: "testConsentString", + consentString: 'testConsentString', }; - const userSyncs = spec.getUserSyncs({}, {}, gdprConsent, {}); - - expect(userSyncs).to.be.empty; + expect(spec.getUserSyncs({}, {}, gdprConsent, {})).to.be.empty; }); - it("user-syncs with enabled pixel option", function () { + it('returns a pixel sync when pixelEnabled and gdprApplies=false', function () { const gdprConsent = { - vendorData: { - purpose: { - consents: 1, - }, - }, + vendorData: { purpose: { consents: 1 } }, gdprApplies: false, - consentString: "testConsentString", + consentString: 'testConsentString', }; - const synOptions = { pixelEnabled: true, iframeEnabled: true }; - const userSyncs = spec.getUserSyncs(synOptions, {}, gdprConsent, {}); - - expect(userSyncs[0].type).to.equal("image"); - expect(userSyncs[0].url).to.equal( - "https://ib.adnxs.com/getuid?https://prebid.cwi.re/v1/cookiesync?xandrId=$UID&gdpr=0&gdpr_consent=testConsentString" + const syncs = spec.getUserSyncs( + { pixelEnabled: true, iframeEnabled: true }, + {}, + gdprConsent, + {} + ); + expect(syncs[0].type).to.equal('image'); + expect(syncs[0].url).to.equal( + 'https://ib.adnxs.com/getuid?https://prebid.cwi.re/v1/cookiesync?xandrId=$UID&gdpr=0&gdpr_consent=testConsentString' ); }); - it("user-syncs with enabled iframe option", function () { + it('returns an iframe sync when only iframeEnabled and gdprApplies=true', function () { const gdprConsent = { - vendorData: { - purpose: { - consents: { - 1: true, - }, - }, - }, + vendorData: { purpose: { consents: { 1: true } } }, gdprApplies: true, - consentString: "abc123", + consentString: 'abc123', }; - const synOptions = { iframeEnabled: true }; - const userSyncs = spec.getUserSyncs(synOptions, {}, gdprConsent, {}); - - expect(userSyncs[0].type).to.equal("iframe"); - expect(userSyncs[0].url).to.equal( - "https://ib.adnxs.com/getuid?https://prebid.cwi.re/v1/cookiesync?xandrId=$UID&gdpr=1&gdpr_consent=abc123" + const syncs = spec.getUserSyncs({ iframeEnabled: true }, {}, gdprConsent, {}); + expect(syncs[0].type).to.equal('iframe'); + expect(syncs[0].url).to.equal( + 'https://ib.adnxs.com/getuid?https://prebid.cwi.re/v1/cookiesync?xandrId=$UID&gdpr=1&gdpr_consent=abc123' ); }); }); - - describe("buildRequests includes autoplay", function () { - afterEach(function () { - sandbox.restore(); - }); - - it("should include autoplay: true when autoplay is enabled", function () { - sandbox.stub(autoplayLib, "isAutoplayEnabled").returns(true); - - const bidRequest = deepClone(bidRequests[0]); - const request = spec.buildRequests([bidRequest], bidderRequest); - const payload = JSON.parse(request.data); - - expect(payload.slots[0].params.autoplay).to.equal(true); - }); - - it("should include autoplay: false when autoplay is disabled", function () { - sandbox.stub(autoplayLib, "isAutoplayEnabled").returns(false); - - const bidRequest = deepClone(bidRequests[0]); - const request = spec.buildRequests([bidRequest], bidderRequest); - const payload = JSON.parse(request.data); - - expect(payload.slots[0].params.autoplay).to.equal(false); - }); - }); - - describe("buildRequests with floor", function () { - it("should include floor in params when getFloor is defined", function () { - const bid = { - bidId: "123", - adUnitCode: "test-div", - mediaTypes: { - banner: { - sizes: [[300, 250]], - }, - }, - params: { - pageId: 4057, - placementId: "abc123", - }, - getFloor: function ({ currency, mediaType, size }) { - expect(currency).to.equal("USD"); - expect(mediaType).to.equal("*"); - expect(size).to.equal("*"); - return { - currency: "USD", - floor: 1.23, - }; - }, - }; - - const bidderRequest = { - refererInfo: { - page: "https://example.com", - }, - }; - - const request = spec.buildRequests([bid], bidderRequest); - - const payload = JSON.parse(request.data); - const slot = payload.slots[0]; - - expect(slot.params).to.have.property("floor"); - expect(slot.params.floor).to.deep.equal({ - currency: "USD", - floor: 1.23, - }); - }); - - it("should not include floor in params if getFloor is not defined", function () { - const bid = { - bidId: "456", - adUnitCode: "test-div", - mediaTypes: { - banner: { - sizes: [[300, 250]], - }, - }, - params: { - pageId: 4057, - placementId: "abc123", - }, - // no getFloor - }; - - const bidderRequest = { - refererInfo: { - page: "https://example.com", - }, - }; - - const request = spec.buildRequests([bid], bidderRequest); - const payload = JSON.parse(request.data); - const slot = payload.slots[0]; - - expect(slot.params.floor).to.deep.equal({}); - }); - }); }); From 4b96fc21220cb873786c35bc893d5f0b2d45a337 Mon Sep 17 00:00:00 2001 From: Milica Grujicic Date: Thu, 28 May 2026 14:02:33 +0200 Subject: [PATCH 3/4] Change endpoint --- modules/cwireBidAdapter.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/cwireBidAdapter.js b/modules/cwireBidAdapter.js index f260a232489..3f321fc26d6 100644 --- a/modules/cwireBidAdapter.js +++ b/modules/cwireBidAdapter.js @@ -17,7 +17,7 @@ import { getAdUnitElement } from '../src/utils/adUnits.js'; const BIDDER_CODE = 'cwire'; const CWID_KEY = 'cw_cwid'; -export const BID_ENDPOINT = 'https://prebid.cwi.re/v2/bid'; +export const BID_ENDPOINT = 'https://ortb.cwi.re/v1/bid'; export const EVENT_ENDPOINT = 'https://prebid.cwi.re/v2/event'; export const GVL_ID = 1081; From 318d68f360fa936ab29f9570a7c495f37bb2dd54 Mon Sep 17 00:00:00 2001 From: Milica Grujicic Date: Thu, 28 May 2026 14:04:52 +0200 Subject: [PATCH 4/4] Missed event endpoint --- modules/cwireBidAdapter.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/cwireBidAdapter.js b/modules/cwireBidAdapter.js index 3f321fc26d6..ac6ebbe7936 100644 --- a/modules/cwireBidAdapter.js +++ b/modules/cwireBidAdapter.js @@ -18,7 +18,7 @@ const BIDDER_CODE = 'cwire'; const CWID_KEY = 'cw_cwid'; export const BID_ENDPOINT = 'https://ortb.cwi.re/v1/bid'; -export const EVENT_ENDPOINT = 'https://prebid.cwi.re/v2/event'; +export const EVENT_ENDPOINT = 'https://prebid.cwi.re/v1/event'; export const GVL_ID = 1081; export const storage = getStorageManager({ bidderCode: BIDDER_CODE });