Skip to content

Commit b2f5532

Browse files
MagicBid Bid Adapter: add new adapter (prebid#14930)
* Add MagicBid bid adapter Add MagicBid bid adapter * Add MagicBid bid adapter tests Add MagicBid bid adapter tests * Add files via upload * Move spec file to correct folder test/spec/modules Move spec file to correct folder test/spec/modules * Delete test/magicbidBidAdapter_spec.js Remove spec file from wrong folder * Add files via upload Move adapter files to correct modules folder * Update magicbidBidAdapter.md contact email id updated * Delete magicbidBidAdapter.js Remove adapter from wrong folder * Delete magicbidBidAdapter.md Remove adapter from wrong folder * Add files via upload magicbid: remove GVLID, infer adUnitType from mediaTypes, fix schain/nurl/playerSize * Add files via upload magicbid: add TypeScript interface, mark adUnitType as optional * Add files via upload magicbid: update tests for inferred adUnitType and new fixes * Add files via upload add TypeScript declaration file * Add files via upload import types from d.ts file * Add files via upload reference d.ts file for types * Add files via upload fix schain/eids per group, register BidderParams type * Add files via upload fix schain/eids per group, register BidderParams type * Add files via upload fix d.ts imports - remove unused type, fix module path * Add files via upload add bidder code generic to BidderSpec to fix TS build * Add files via upload remove BidderSpec export to fix TS2314 build error
1 parent 41285a6 commit b2f5532

4 files changed

Lines changed: 775 additions & 0 deletions

File tree

modules/magicbidBidAdapter.d.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
export interface MagicBidParams {
2+
/** Publisher-specific RTB host provided by MagicBid (unique per publisher, always ends with .rtb-magicbid.ai) */
3+
host: string;
4+
/** Ad Unit ID for this placement, provided by MagicBid */
5+
adUnitId: number;
6+
/** Ad format: 'banner' or 'video'. If omitted, inferred from mediaTypes. */
7+
adUnitType?: string;
8+
/** Publisher ID — required only for Prebid Server / server-side */
9+
publisherId?: string;
10+
/** Custom targeting parameter 1 */
11+
custom1?: string;
12+
/** Custom targeting parameter 2 */
13+
custom2?: string;
14+
/** Custom targeting parameter 3 */
15+
custom3?: string;
16+
/** Custom targeting parameter 4 */
17+
custom4?: string;
18+
/** Custom targeting parameter 5 */
19+
custom5?: string;
20+
}
21+
22+
declare module '../src/adUnits.js' {
23+
interface BidderParams {
24+
magicbid: MagicBidParams;
25+
}
26+
}

modules/magicbidBidAdapter.js

Lines changed: 308 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,308 @@
1+
'use strict';
2+
3+
import { registerBidder } from '../src/adapters/bidderFactory.js';
4+
import { BANNER, VIDEO } from '../src/mediaTypes.js';
5+
import { deepAccess, logWarn } from '../src/utils.js';
6+
/** @import { MagicBidParams } from './magicbidBidAdapter.d.ts' */
7+
8+
// ─────────────────────────────────────────────
9+
// MagicBid Prebid.js Bid Adapter
10+
// Bidder Code : magicbid
11+
// Supported : Banner, Video
12+
// ─────────────────────────────────────────────
13+
//
14+
// Each publisher receives TWO values from MagicBid:
15+
// host → e.g. 'ads-2j0kac.rtb-magicbid.ai' (unique per publisher,
16+
// generated by the Limelight platform when a new publisher
17+
// endpoint is created)
18+
// adUnitId → e.g. 631967104 (unique per ad placement)
19+
//
20+
// adUnitType is NOT required — the adapter infers it automatically
21+
// from the mediaTypes object on the ad unit.
22+
// ─────────────────────────────────────────────
23+
24+
const BIDDER_CODE = 'magicbid';
25+
26+
// ─── Helpers ────────────────────────────────
27+
28+
/**
29+
* Determine adUnitType from the bid's mediaTypes object.
30+
* VIDEO takes priority if both are present.
31+
* The host is NOT static — it is unique per publisher and is provided
32+
* by MagicBid during onboarding (generated by the Limelight platform).
33+
*/
34+
function resolveAdUnitType(bid) {
35+
if (deepAccess(bid, 'mediaTypes.video')) return VIDEO;
36+
if (deepAccess(bid, 'mediaTypes.banner')) return BANNER;
37+
// Fall back to explicit param if mediaTypes is not set
38+
const paramType = (bid.params.adUnitType || '').toLowerCase();
39+
if (paramType === VIDEO || paramType === BANNER) return paramType;
40+
return BANNER;
41+
}
42+
43+
/**
44+
* Build the RTB endpoint URL from the publisher-supplied host.
45+
* The host is publisher-specific and NOT static — each publisher
46+
* receives a unique host from MagicBid during onboarding.
47+
* e.g. host = 'ads-2j0kac.rtb-magicbid.ai' (unique per publisher)
48+
* type = 'banner'
49+
* → 'https://ads-2j0kac.rtb-magicbid.ai/prebid/banner'
50+
*/
51+
function buildEndpointUrl(host, adUnitType) {
52+
const path = adUnitType === VIDEO ? 'vast' : 'banner';
53+
return 'https://' + host + '/prebid/' + path;
54+
}
55+
56+
/**
57+
* Validate that a required string parameter exists and is non-empty.
58+
*/
59+
function validateStringParam(params, paramName) {
60+
const val = params[paramName];
61+
if (typeof val !== 'string' || val.trim() === '') {
62+
logWarn('[MagicBid] Missing or invalid required param: "' + paramName + '"');
63+
return false;
64+
}
65+
return true;
66+
}
67+
68+
/**
69+
* Validate that a required integer param is a positive integer.
70+
*/
71+
function validateIntParam(params, paramName) {
72+
const val = params[paramName];
73+
if (!Number.isInteger(val) || val <= 0) {
74+
logWarn('[MagicBid] Missing or invalid required param: "' + paramName + '" (must be a positive integer)');
75+
return false;
76+
}
77+
return true;
78+
}
79+
80+
/**
81+
* Normalize video playerSize — handles both flat [w, h] and
82+
* nested [[w, h]] forms that publishers may use.
83+
*/
84+
function normalizePlayerSize(playerSize) {
85+
if (!playerSize) return [];
86+
// Nested: [[640, 480]] → return as-is
87+
if (Array.isArray(playerSize[0])) return playerSize;
88+
// Flat: [640, 480] → wrap in array
89+
return [playerSize];
90+
}
91+
92+
// ─── Adapter Definition ──────────────────────
93+
94+
export const spec = {
95+
code: BIDDER_CODE,
96+
supportedMediaTypes: [BANNER, VIDEO],
97+
98+
// ─── 1. isBidRequestValid ───────────────────
99+
/**
100+
* Required params: host, adUnitId
101+
* adUnitType is optional — inferred from mediaTypes automatically
102+
*/
103+
isBidRequestValid(bid) {
104+
const params = bid.params || {};
105+
106+
if (!validateStringParam(params, 'host')) return false;
107+
if (!validateIntParam(params, 'adUnitId')) return false;
108+
109+
// If adUnitType is explicitly provided, validate it
110+
if (params.adUnitType) {
111+
const t = params.adUnitType.toLowerCase();
112+
if (t !== BANNER && t !== VIDEO) {
113+
logWarn('[MagicBid] adUnitType must be "banner" or "video", got: "' + params.adUnitType + '"');
114+
return false;
115+
}
116+
}
117+
118+
return true;
119+
},
120+
121+
// ─── 2. buildRequests ───────────────────────
122+
/**
123+
* Group valid bid requests by host+adUnitType combination and
124+
* fire one HTTP request per group.
125+
*/
126+
buildRequests(validBidRequests, bidderRequest) {
127+
const groups = {};
128+
129+
validBidRequests.forEach(function(bid) {
130+
const params = bid.params;
131+
const host = params.host;
132+
const adUnitId = params.adUnitId;
133+
134+
// Infer adUnitType from mediaTypes — no longer required as a param
135+
const resolvedType = resolveAdUnitType(bid);
136+
137+
const key = host + '__' + resolvedType;
138+
139+
if (!groups[key]) {
140+
groups[key] = {
141+
url: buildEndpointUrl(host, resolvedType),
142+
adUnitType: resolvedType,
143+
bids: [],
144+
};
145+
}
146+
147+
// Collect ad sizes
148+
let sizes = [];
149+
if (resolvedType === BANNER) {
150+
sizes = deepAccess(bid, 'mediaTypes.banner.sizes') || bid.sizes || [];
151+
} else if (resolvedType === VIDEO) {
152+
const playerSize = deepAccess(bid, 'mediaTypes.video.playerSize');
153+
sizes = normalizePlayerSize(playerSize);
154+
}
155+
156+
const bidData = {
157+
bidId: bid.bidId,
158+
transactionId: deepAccess(bid, 'ortb2Imp.ext.tid') || bid.transactionId,
159+
adUnitCode: bid.adUnitCode,
160+
adUnitId: adUnitId,
161+
sizes: sizes,
162+
// Internal reference — stripped before sending to server
163+
_sourceBid: bid,
164+
};
165+
166+
// Optional params
167+
if (params.publisherId) bidData.publisherId = params.publisherId;
168+
if (params.custom1) bidData.custom1 = params.custom1;
169+
if (params.custom2) bidData.custom2 = params.custom2;
170+
if (params.custom3) bidData.custom3 = params.custom3;
171+
if (params.custom4) bidData.custom4 = params.custom4;
172+
if (params.custom5) bidData.custom5 = params.custom5;
173+
174+
groups[key].bids.push(bidData);
175+
});
176+
177+
return Object.values(groups).map(function(group) {
178+
// Use the first bid from THIS group (not the whole auction)
179+
// to avoid mixing schain/eids from different publishers
180+
const representativeBid = group.bids[0]._sourceBid;
181+
182+
// Read schain from ORTB source path (preferred) or legacy field
183+
const schain = deepAccess(representativeBid, 'ortb2.source.ext.schain') ||
184+
deepAccess(bidderRequest, 'ortb2.source.ext.schain') ||
185+
representativeBid.schain || null;
186+
187+
const eids = representativeBid.userIdAsEids || [];
188+
189+
return {
190+
method: 'POST',
191+
url: group.url,
192+
data: JSON.stringify({
193+
id: bidderRequest.auctionId,
194+
site: {
195+
page: deepAccess(bidderRequest, 'refererInfo.page') || window.location.href,
196+
ref: deepAccess(bidderRequest, 'refererInfo.ref') || document.referrer,
197+
},
198+
bids: group.bids.map(function(b) {
199+
const clean = Object.assign({}, b);
200+
delete clean._sourceBid;
201+
return clean;
202+
}),
203+
gdpr: {
204+
applies: !!(deepAccess(bidderRequest, 'gdprConsent.gdprApplies')),
205+
consent: deepAccess(bidderRequest, 'gdprConsent.consentString') || '',
206+
},
207+
usp: bidderRequest.uspConsent || '',
208+
coppa: !!(deepAccess(bidderRequest, 'ortb2.regs.coppa')),
209+
gpp: deepAccess(bidderRequest, 'gppConsent.gppString') || '',
210+
gppSid: deepAccess(bidderRequest, 'gppConsent.applicableSections') || [],
211+
schain: schain,
212+
eids: eids,
213+
}),
214+
options: {
215+
// Using text/plain to avoid CORS preflight requests which can
216+
// increase bidder timeouts. The server must accept text/plain.
217+
contentType: 'text/plain',
218+
withCredentials: true,
219+
},
220+
_adUnitType: group.adUnitType,
221+
};
222+
});
223+
},
224+
225+
// ─── 3. interpretResponse ───────────────────
226+
interpretResponse(serverResponse, request) {
227+
const bids = [];
228+
229+
if (!serverResponse || !serverResponse.body) {
230+
logWarn('[MagicBid] Empty or missing response body');
231+
return bids;
232+
}
233+
234+
const adUnitType = request._adUnitType;
235+
const rawBids = Array.isArray(serverResponse.body)
236+
? serverResponse.body
237+
: [serverResponse.body];
238+
239+
rawBids.forEach(function(rawBid) {
240+
if (!rawBid || !rawBid.cpm || rawBid.cpm <= 0) return;
241+
242+
const bid = {
243+
requestId: rawBid.bidId,
244+
cpm: rawBid.cpm,
245+
currency: rawBid.currency || 'USD',
246+
width: rawBid.width || 0,
247+
height: rawBid.height || 0,
248+
creativeId: rawBid.creativeId || rawBid.bidId,
249+
dealId: rawBid.dealId || null,
250+
netRevenue: true,
251+
ttl: rawBid.ttl || 300,
252+
// Preserve nurl for win notification in onBidWon
253+
nurl: rawBid.nurl || null,
254+
meta: {
255+
advertiserDomains: rawBid.adomain || [],
256+
},
257+
};
258+
259+
if (adUnitType === VIDEO) {
260+
bid.mediaType = VIDEO;
261+
bid.vastUrl = rawBid.vastUrl || rawBid.ad;
262+
bid.vastXml = rawBid.vastXml || null;
263+
} else {
264+
bid.mediaType = BANNER;
265+
bid.ad = rawBid.ad;
266+
}
267+
268+
bids.push(bid);
269+
});
270+
271+
return bids;
272+
},
273+
274+
// ─── 4. getUserSyncs ────────────────────────
275+
getUserSyncs(syncOptions, serverResponses) {
276+
const syncs = [];
277+
if (!serverResponses || serverResponses.length === 0) return syncs;
278+
279+
serverResponses.forEach(function(response) {
280+
const syncData = deepAccess(response, 'body.userSyncs');
281+
if (!Array.isArray(syncData)) return;
282+
283+
syncData.forEach(function(sync) {
284+
if (sync.type === 'image' && syncOptions.pixelEnabled) {
285+
syncs.push({ type: 'image', url: sync.url });
286+
} else if (sync.type === 'iframe' && syncOptions.iframeEnabled) {
287+
syncs.push({ type: 'iframe', url: sync.url });
288+
}
289+
});
290+
});
291+
292+
return syncs;
293+
},
294+
295+
// ─── 5. onBidWon ────────────────────────────
296+
onBidWon(bid) {
297+
if (bid.nurl) {
298+
fetch(bid.nurl, { method: 'GET', keepalive: true }).catch(function() {});
299+
}
300+
},
301+
302+
// ─── 6. onTimeout ───────────────────────────
303+
onTimeout(timeoutData) {
304+
logWarn('[MagicBid] Bid timed out:', timeoutData);
305+
},
306+
};
307+
308+
registerBidder(spec);

0 commit comments

Comments
 (0)