diff --git a/integrationExamples/gpt/adcluster_banner_example.html b/integrationExamples/gpt/adcluster_banner_example.html new file mode 100644 index 0000000000..4f7bf646bb --- /dev/null +++ b/integrationExamples/gpt/adcluster_banner_example.html @@ -0,0 +1,115 @@ + + + + + Adcluster Adapter Test + + + + + + + + + + +

Prebid.js Live Adapter Test

+
+ + + + diff --git a/integrationExamples/gpt/adcluster_video_example.html b/integrationExamples/gpt/adcluster_video_example.html new file mode 100644 index 0000000000..0a309b2474 --- /dev/null +++ b/integrationExamples/gpt/adcluster_video_example.html @@ -0,0 +1,291 @@ + + + + + Adcluster Adapter – Outstream Test with Fallback + + + + + + + + + +

Adcluster Adapter – Outstream Test (AN renderer + IMA fallback)

+
+ +
+ + + + diff --git a/modules/adclusterBidAdapter.js b/modules/adclusterBidAdapter.js new file mode 100644 index 0000000000..b8b1f24858 --- /dev/null +++ b/modules/adclusterBidAdapter.js @@ -0,0 +1,183 @@ +import { registerBidder } from "../src/adapters/bidderFactory.js"; +import { BANNER, VIDEO } from "../src/mediaTypes.js"; + +const BIDDER_CODE = "adcluster"; +const ENDPOINT = "https://core.adcluster.com.tr/bid"; + +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: [BANNER, VIDEO], + + isBidRequestValid(bid) { + return !!bid?.params?.unitId; + }, + + buildRequests(validBidRequests, bidderRequest) { + const _auctionId = bidderRequest.auctionId || ""; + const payload = { + bidderCode: bidderRequest.bidderCode, + auctionId: _auctionId, + bidderRequestId: bidderRequest.bidderRequestId, + bids: validBidRequests.map((b) => buildImp(b)), + auctionStart: bidderRequest.auctionStart, + timeout: bidderRequest.timeout, + start: bidderRequest.start, + regs: { ext: {} }, + user: { ext: {} }, + source: { ext: {} }, + }; + + // privacy + if (bidderRequest?.gdprConsent) { + payload.regs = payload.regs || { ext: {} }; + payload.regs.ext = payload.regs.ext || {}; + payload.regs.ext.gdpr = bidderRequest.gdprConsent.gdprApplies ? 1 : 0; + payload.user.ext.consent = bidderRequest.gdprConsent.consentString || ""; + } + if (bidderRequest?.uspConsent) { + payload.regs = payload.regs || { ext: {} }; + payload.regs.ext.us_privacy = bidderRequest.uspConsent; + } + if (bidderRequest?.ortb2?.regs?.gpp) { + payload.regs = payload.regs || { ext: {} }; + payload.regs.ext.gpp = bidderRequest.ortb2.regs.gpp; + payload.regs.ext.gppSid = bidderRequest.ortb2.regs.gpp_sid; + } + if (validBidRequests[0]?.userIdAsEids) { + payload.user.ext.eids = validBidRequests[0].userIdAsEids; + } + if (validBidRequests[0]?.ortb2?.source?.ext?.schain) { + payload.source.ext.schain = validBidRequests[0].ortb2.source.ext.schain; + } + + return { + method: "POST", + url: ENDPOINT, + data: payload, + options: { contentType: "text/plain" }, + }; + }, + + interpretResponse(serverResponse) { + const body = serverResponse?.body; + if (!body || !Array.isArray(body)) return []; + const bids = []; + + body.forEach((b) => { + const mediaType = detectMediaType(b); + const bid = { + requestId: b.requestId, + cpm: b.cpm, + currency: b.currency, + width: b.width, + height: b.height, + creativeId: b.creativeId, + ttl: b.ttl, + netRevenue: b.netRevenue, + meta: { + advertiserDomains: b.meta?.advertiserDomains || [], + }, + mediaType, + }; + + if (mediaType === BANNER) { + bid.ad = b.ad; + } + if (mediaType === VIDEO) { + bid.vastUrl = b.ad; + } + bids.push(bid); + }); + + return bids; + }, +}; + +/* ---------- helpers ---------- */ + +function buildImp(bid) { + const _transactionId = bid.transactionId || ""; + const _adUnitId = bid.adUnitId || ""; + const _auctionId = bid.auctionId || ""; + const imp = { + params: { + unitId: bid.params.unitId, + }, + bidId: bid.bidId, + bidderRequestId: bid.bidderRequestId, + transactionId: _transactionId, + adUnitId: _adUnitId, + auctionId: _auctionId, + ext: { + floors: getFloorsAny(bid), + }, + }; + + if (bid.params && bid.params.previewMediaId) { + imp.params.previewMediaId = bid.params.previewMediaId; + } + + const mt = bid.mediaTypes || {}; + + // BANNER + if (mt.banner?.sizes?.length) { + imp.width = mt.banner.sizes[0] && mt.banner.sizes[0][0]; + imp.height = mt.banner.sizes[0] && mt.banner.sizes[0][1]; + } + if (mt.video) { + const v = mt.video; + const playerSize = toSizeArray(v.playerSize); + const [vw, vh] = playerSize?.[0] || []; + imp.width = vw; + imp.height = vh; + imp.video = { + minduration: v.minduration || 1, + maxduration: v.maxduration || 120, + ext: { + context: v.context || "instream", + floor: getFloors(bid, "video", playerSize?.[0]), + }, + }; + } + + return imp; +} + +function toSizeArray(s) { + if (!s) return null; + // playerSize can be [w,h] or [[w,h], [w2,h2]] + return Array.isArray(s[0]) ? s : [s]; +} + +function getFloors(bid, mediaType = "banner", size) { + try { + if (!bid.getFloor) return null; + // size can be [w,h] or '*' + const sz = Array.isArray(size) ? size : "*"; + const res = bid.getFloor({ mediaType, size: sz }); + return res && typeof res.floor === "number" ? res.floor : null; + } catch { + return null; + } +} + +function detectMediaType(bid) { + if (bid.mediaType === "video") return VIDEO; + else return BANNER; +} + +function getFloorsAny(bid) { + // Try to collect floors per type + const out = {}; + const mt = bid.mediaTypes || {}; + if (mt.banner) { + out.banner = getFloors(bid, "banner", "*"); + } + if (mt.video) { + const ps = toSizeArray(mt.video.playerSize); + out.video = getFloors(bid, "video", (ps && ps[0]) || "*"); + } + return out; +} + +registerBidder(spec); diff --git a/modules/adclusterBidAdapter.md b/modules/adclusterBidAdapter.md new file mode 100644 index 0000000000..59300e2c85 --- /dev/null +++ b/modules/adclusterBidAdapter.md @@ -0,0 +1,46 @@ +# Overview + +**Module Name**: Adcluster Bidder Adapter +**Module Type**: Bidder Adapter +**Maintainer**: dev@adcluster.com.tr + +# Description + +Prebid.js bidder adapter module for connecting to Adcluster. + +# Test Parameters + +``` +var adUnits = [ + { + code: 'adcluster-banner', + mediaTypes: { + banner: { + sizes: [[300, 250]], + } + }, + bids: [{ + bidder: 'adcluster', + params: { + unitId: '42d1f525-5792-47a6-846d-1825e53c97d6', + previewMediaId: "b4dbc48c-0b90-4628-bc55-f46322b89b63", + }, + }] + }, + { + code: 'adcluster-video', + mediaTypes: { + video: { + playerSize: [[640, 480]], + } + }, + bids: [{ + bidder: 'adcluster', + params: { + unitId: "37dd91b2-049d-4027-94b9-d63760fc10d3", + previewMediaId: "133b7dc9-bb6e-4ab2-8f95-b796cf19f27e", + }, + }] + } +]; +``` diff --git a/test/spec/modules/adclusterBidAdapter_spec.js b/test/spec/modules/adclusterBidAdapter_spec.js new file mode 100644 index 0000000000..d068a0b594 --- /dev/null +++ b/test/spec/modules/adclusterBidAdapter_spec.js @@ -0,0 +1,415 @@ +// test/spec/modules/adclusterBidAdapter_spec.js + +import { expect } from "chai"; +import { spec } from "modules/adclusterBidAdapter.js"; // adjust path if needed +import { BANNER, VIDEO } from "src/mediaTypes.js"; + +const BIDDER_CODE = "adcluster"; +const ENDPOINT = "https://core.adcluster.com.tr/bid"; + +describe("adclusterBidAdapter", function () { + // ---------- Test Fixtures (immutable) ---------- + const baseBid = Object.freeze({ + bidder: BIDDER_CODE, + bidId: "2f5d", + bidderRequestId: "breq-1", + auctionId: "auc-1", + transactionId: "txn-1", + adUnitCode: "div-1", + adUnitId: "adunit-1", + params: { unitId: "61884b5c-9420-4f15-871f-2dcc2fa1cff5" }, + }); + + const gdprConsent = Object.freeze({ + gdprApplies: true, + consentString: "BOJ/P2HOJ/P2HABABMAAAAAZ+A==", + }); + + const uspConsent = "1---"; + const gpp = "DBABLA.."; + const gppSid = [7, 8]; + + const bidderRequestBase = Object.freeze({ + auctionId: "auc-1", + bidderCode: BIDDER_CODE, + bidderRequestId: "breq-1", + auctionStart: 1111111111111, + timeout: 2000, + start: 1111111111112, + ortb2: { regs: { gpp, gpp_sid: gppSid } }, + gdprConsent, + uspConsent, + }); + + // helpers return fresh objects to avoid cross-test mutation + function mkBidBanner(extra = {}) { + return { + ...baseBid, + mediaTypes: { + banner: { + sizes: [ + [300, 250], + [300, 600], + ], + }, + }, + getFloor: ({ mediaType }) => { + if (mediaType === "banner") return { currency: "USD", floor: 0.5 }; + return { currency: "USD", floor: 0.0 }; + }, + userIdAsEids: [ + { source: "example.com", uids: [{ id: "abc", atype: 1 }] }, + ], + ortb2: { + source: { ext: { schain: { ver: "1.0", complete: 1, nodes: [] } } }, + }, + ...extra, + }; + } + + function mkBidVideo(extra = {}) { + return { + ...baseBid, + mediaTypes: { + video: { + context: "instream", + playerSize: [640, 360], + minduration: 5, + maxduration: 30, + }, + }, + getFloor: ({ mediaType, size }) => { + if (mediaType === "video" && Array.isArray(size)) { + return { currency: "USD", floor: 1.2 }; + } + return { currency: "USD", floor: 0.0 }; + }, + userIdAsEids: [ + { source: "example.com", uids: [{ id: "xyz", atype: 1 }] }, + ], + ortb2: { + source: { ext: { schain: { ver: "1.0", complete: 1, nodes: [] } } }, + }, + ...extra, + }; + } + + describe("isBidRequestValid", function () { + it("returns true when params.unitId is present", function () { + // Arrange + const bid = { ...baseBid }; + + // Act + const valid = spec.isBidRequestValid(bid); + + // Assert + expect(valid).to.equal(true); + }); + + it("returns false when params.unitId is missing", function () { + // Arrange + const bid = { ...baseBid, params: {} }; + + // Act + const valid = spec.isBidRequestValid(bid); + + // Assert + expect(valid).to.equal(false); + }); + + it("returns false when params is undefined", function () { + // Arrange + const bid = { ...baseBid, params: undefined }; + + // Act + const valid = spec.isBidRequestValid(bid); + + // Assert + expect(valid).to.equal(false); + }); + }); + + describe("buildRequests", function () { + it("builds a POST request with JSON body to the right endpoint", function () { + // Arrange + const br = { ...bidderRequestBase }; + const bids = [mkBidBanner(), mkBidVideo()]; + + // Act + const req = spec.buildRequests(bids, br); + + // Assert + expect(req.method).to.equal("POST"); + expect(req.url).to.equal(ENDPOINT); + expect(req.options).to.deep.equal({ contentType: "text/plain" }); + expect(req.data).to.be.an("object"); + expect(req.data.bidderCode).to.equal(BIDDER_CODE); + expect(req.data.auctionId).to.equal(br.auctionId); + expect(req.data.bids).to.be.an("array").with.length(2); + }); + + it("includes privacy signals (GDPR, USP, GPP) when present", function () { + // Arrange + const br = { ...bidderRequestBase }; + const bids = [mkBidBanner()]; + + // Act + const req = spec.buildRequests(bids, br); + + // Assert + const { regs, user } = req.data; + expect(regs).to.be.an("object"); + expect(regs.ext.gdpr).to.equal(1); + expect(user.ext.consent).to.equal(gdprConsent.consentString); + expect(regs.ext.us_privacy).to.equal(uspConsent); + expect(regs.ext.gpp).to.equal(gpp); + expect(regs.ext.gppSid).to.deep.equal(gppSid); + }); + + it("omits privacy fields when not provided", function () { + // Arrange + const minimalBR = { + auctionId: "auc-2", + bidderCode: BIDDER_CODE, + bidderRequestId: "breq-2", + auctionStart: 1, + timeout: 1000, + start: 2, + }; + const bids = [mkBidBanner()]; + + // Act + const req = spec.buildRequests(bids, minimalBR); + + // Assert + // regs.ext should exist but contain no privacy flags + expect(req.data.regs).to.be.an("object"); + expect(req.data.regs.ext).to.deep.equal({}); + // user.ext.consent must be undefined when no GDPR + expect(req.data.user).to.be.an("object"); + expect(req.data.user.ext).to.be.an("object"); + expect(req.data.user.ext.consent).to.be.undefined; + // allow eids to be present (they come from bids) + // don't assert deep-equality on user.ext, just ensure no privacy fields + expect(req.data.user.ext.gdpr).to.be.undefined; + }); + + it("passes userIdAsEids and schain when provided", function () { + // Arrange + const br = { ...bidderRequestBase }; + const bids = [mkBidBanner()]; + + // Act + const req = spec.buildRequests(bids, br); + + // Assert + expect(req.data.user.ext.eids).to.be.an("array").with.length(1); + expect(req.data.source.ext.schain).to.be.an("object"); + }); + + it("sets banner dimensions from first size and includes floors ext", function () { + // Arrange + const br = { ...bidderRequestBase }; + const bids = [mkBidBanner()]; + + // Act + const req = spec.buildRequests(bids, br); + + // Assert + const imp = req.data.bids[0]; + expect(imp.width).to.equal(300); + expect(imp.height).to.equal(250); + expect(imp.ext).to.have.property("floors"); + expect(imp.ext.floors.banner).to.equal(0.5); + }); + + it("sets video sizes from playerSize and includes video floors", function () { + // Arrange + const br = { ...bidderRequestBase }; + const bids = [mkBidVideo()]; + + // Act + const req = spec.buildRequests(bids, br); + + // Assert + const imp = req.data.bids[0]; + expect(imp.width).to.equal(640); + expect(imp.height).to.equal(360); + expect(imp.video).to.be.an("object"); + expect(imp.video.minduration).to.equal(5); + expect(imp.video.maxduration).to.equal(30); + expect(imp.video.ext.context).to.equal("instream"); + expect(imp.video.ext.floor).to.equal(1.2); + expect(imp.ext.floors.video).to.equal(1.2); + }); + + it("gracefully handles missing getFloor", function () { + // Arrange + const br = { ...bidderRequestBase }; + const bids = [mkBidBanner({ getFloor: undefined })]; + + // Act + const req = spec.buildRequests(bids, br); + + // Assert + expect(req.data.bids[0].ext.floors.banner).to.equal(null); + }); + + it("passes previewMediaId when provided", function () { + // Arrange + const br = { ...bidderRequestBase }; + const bids = [ + mkBidVideo({ params: { unitId: "x", previewMediaId: "media-123" } }), + ]; + + // Act + const req = spec.buildRequests(bids, br); + + // Assert + expect(req.data.bids[0].params.previewMediaId).to.equal("media-123"); + }); + }); + + describe("interpretResponse", function () { + it("returns empty array when body is missing or not an array", function () { + // Arrange + const missing = { body: null }; + const notArray = { body: {} }; + + // Act + const out1 = spec.interpretResponse(missing); + const out2 = spec.interpretResponse(notArray); + + // Assert + expect(out1).to.deep.equal([]); + expect(out2).to.deep.equal([]); + }); + + it("maps banner responses to Prebid bids", function () { + // Arrange + const serverBody = [ + { + requestId: "2f5d", + cpm: 1.23, + currency: "USD", + width: 300, + height: 250, + creativeId: "cr-1", + ttl: 300, + netRevenue: true, + mediaType: "banner", + ad: "
creative
", + meta: { advertiserDomains: ["advertiser.com"] }, + }, + ]; + + // Act + const out = spec.interpretResponse({ body: serverBody }); + + // Assert + expect(out).to.have.length(1); + const b = out[0]; + expect(b.requestId).to.equal("2f5d"); + expect(b.cpm).to.equal(1.23); + expect(b.mediaType).to.equal(BANNER); + expect(b.ad).to.be.a("string"); + expect(b.meta.advertiserDomains).to.deep.equal(["advertiser.com"]); + }); + + it("maps video responses to Prebid bids (vastUrl)", function () { + // Arrange + const serverBody = [ + { + requestId: "vid-1", + cpm: 2.5, + currency: "USD", + width: 640, + height: 360, + creativeId: "cr-v", + ttl: 300, + netRevenue: true, + mediaType: "video", + ad: "https://vast.tag/url.xml", + meta: { advertiserDomains: ["brand.com"] }, // mediaType hint optional + }, + ]; + + // Act + const out = spec.interpretResponse({ body: serverBody }); + + // Assert + expect(out).to.have.length(1); + const b = out[0]; + expect(b.requestId).to.equal("vid-1"); + expect(b.mediaType).to.equal(VIDEO); + expect(b.vastUrl).to.equal("https://vast.tag/url.xml"); + expect(b.ad).to.be.undefined; + }); + + it("handles missing meta.advertiserDomains safely", function () { + // Arrange + const serverBody = [ + { + requestId: "2f5d", + cpm: 0.2, + currency: "USD", + width: 300, + height: 250, + creativeId: "cr-2", + ttl: 120, + netRevenue: true, + ad: "
", + meta: {}, + }, + ]; + + // Act + const out = spec.interpretResponse({ body: serverBody }); + + // Assert + expect(out[0].meta.advertiserDomains).to.deep.equal([]); + }); + + it("supports multiple mixed responses", function () { + // Arrange + const serverBody = [ + { + requestId: "b-1", + cpm: 0.8, + currency: "USD", + width: 300, + height: 250, + creativeId: "cr-b", + ttl: 300, + netRevenue: true, + ad: "
banner
", + mediaType: "banner", + meta: { advertiserDomains: [] }, + }, + { + requestId: "v-1", + cpm: 3.1, + currency: "USD", + width: 640, + height: 360, + creativeId: "cr-v", + ttl: 300, + netRevenue: true, + mediaType: "video", + ad: "https://vast.example/vast.xml", + meta: { advertiserDomains: ["x.com"] }, + }, + ]; + + // Act + const out = spec.interpretResponse({ body: serverBody }); + + // Assert + expect(out).to.have.length(2); + const [b, v] = out; + expect(b.mediaType).to.equal(BANNER); + expect(v.mediaType).to.equal(VIDEO); + expect(v.vastUrl).to.match(/^https:\/\/vast\.example/); + }); + }); +});