Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 70 additions & 8 deletions modules/bridgewellBidAdapter.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,33 +45,82 @@ export const spec = {
validBidRequests = convertOrtbRequestToProprietaryNative(validBidRequests);

const adUnits = [];
var bidderUrl = REQUEST_ENDPOINT + Math.random();
const bidderUrl = REQUEST_ENDPOINT + Math.random();

_each(validBidRequests, function (bid) {
const passthrough = bid.ortb2Imp?.ext?.prebid?.passthrough;
const filteredPassthrough = passthrough ? Object.fromEntries(
Object.entries({
bucket: passthrough.bucket,
client: passthrough.client,
gamAdCode: passthrough.gamAdCode,
gamLoc: passthrough.gamLoc,
colo: passthrough.colo,
device: passthrough.device,
lang: passthrough.lang,
pt: passthrough.pt,
region: passthrough.region,
site: passthrough.site,
ver: passthrough.ver
}).filter(([_, value]) => value !== undefined)
) : undefined;

const adUnit = {
adUnitCode: bid.adUnitCode,
requestId: bid.bidId,
transactionId: bid.transactionId,
adUnitId: bid.adUnitId,
sizes: bid.sizes,
mediaTypes: bid.mediaTypes || {
banner: {
sizes: bid.sizes
}
},
userIds: bid.userId || {},
userIdAsEids: bid.userIdAsEids || {}
ortb2Imp: {
ext: {
prebid: {
passthrough: filteredPassthrough
},
data: {
adserver: {
name: bid.ortb2Imp?.ext?.data?.adserver?.name,
adslot: bid.ortb2Imp?.ext?.data?.adserver?.adslot
},
pbadslot: bid.ortb2Imp?.ext?.data?.pbadslot
},
gpid: bid.ortb2Imp?.ext?.gpid
},
banner: {
pos: bid.ortb2Imp?.banner?.pos
}
}
};
if (bid.params.cid) {

if (bid.params?.cid) {
adUnit.cid = bid.params.cid;
} else {
} else if (bid.params?.ChannelID) {
adUnit.ChannelID = bid.params.ChannelID;
}

let floorInfo = {};
if (typeof bid.getFloor === 'function') {
const mediaType = bid.mediaTypes?.banner ? BANNER : (bid.mediaTypes?.native ? NATIVE : '*');
const sizes = bid.mediaTypes?.banner?.sizes || bid.sizes || [];
const size = sizes.length === 1 ? sizes[0] : '*';
floorInfo = bid.getFloor({currency: 'USD', mediaType: mediaType, size: size}) || {};
}
adUnit.floor = floorInfo.floor;
adUnit.currency = floorInfo.currency;
adUnits.push(adUnit);
});

let topUrl = '';
if (bidderRequest && bidderRequest.refererInfo) {
if (bidderRequest?.refererInfo?.page) {
topUrl = bidderRequest.refererInfo.page;
}

const firstBid = validBidRequests[0] || {};

return {
method: 'POST',
url: bidderUrl,
Expand All @@ -82,10 +131,23 @@ export const spec = {
},
inIframe: inIframe(),
url: topUrl,
referrer: bidderRequest.refererInfo.ref,
referrer: bidderRequest?.refererInfo?.ref,
auctionId: firstBid?.auctionId,
bidderRequestId: firstBid?.bidderRequestId,
src: firstBid?.src,
userIds: firstBid?.userId || {},
userIdAsEids: firstBid?.userIdAsEids || [],
auctionsCount: firstBid?.auctionsCount,
bidRequestsCount: firstBid?.bidRequestsCount,
bidderRequestsCount: firstBid?.bidderRequestsCount,
bidderWinsCount: firstBid?.bidderWinsCount,
deferBilling: firstBid?.deferBilling,
metrics: firstBid?.metrics || {},
adUnits: adUnits,
// TODO: please do not send internal data structures over the network
refererInfo: bidderRequest.refererInfo.legacy},
refererInfo: bidderRequest?.refererInfo?.legacy,
ortb2: bidderRequest?.ortb2
},
validBidRequests: validBidRequests
};
},
Expand Down
125 changes: 112 additions & 13 deletions modules/insticatorBidAdapter.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {deepAccess, generateUUID, logError, isArray, isInteger, isArrayOfNums, d
import {getStorageManager} from '../src/storageManager.js';

const BIDDER_CODE = 'insticator';
const ENDPOINT = 'https://ex.ingage.tech/v1/openrtb'; // production endpoint
const ENDPOINT = 'https://ex.ingage.tech/v1/openrtb';
const USER_ID_KEY = 'hb_insticator_uid';
const USER_ID_COOKIE_EXP = 2592000000; // 30 days
const BID_TTL = 300; // 5 minutes
Expand All @@ -29,7 +29,16 @@ export const OPTIONAL_VIDEO_PARAMS = {
'playbackend': (value) => isInteger(value) && [1, 2, 3].includes(value),
'delivery': (value) => isArrayOfNums(value),
'pos': (value) => isInteger(value) && [0, 1, 2, 3, 4, 5, 6, 7].includes(value),
'api': (value) => isArrayOfNums(value)};
'api': (value) => isArrayOfNums(value),
// Ad Pod specific parameters (ORTB 2.6)
'podid': (value) => typeof value === 'string' && value.length > 0,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we won't reject requests right if this validations fail? Just wondering if something can be done to still process the requests

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, requests are NOT rejected. this validation only controls whether the param is included and invalid params are skipped, and the bid request still proceeds with valid params.

'podseq': (value) => isInteger(value) && value >= 0,
'poddur': (value) => isInteger(value) && value > 0,
'slotinpod': (value) => isInteger(value) && [-1, 0, 1, 2].includes(value),
'mincpmpersec': (value) => typeof value === 'number' && value > 0,
'maxseq': (value) => isInteger(value) && value > 0,
'rqddurs': (value) => isArrayOfNums(value) && value.every(v => v > 0),
};

const ORTB_SITE_FIRST_PARTY_DATA = {
'cat': v => Array.isArray(v) && v.every(c => typeof c === 'string'),
Expand Down Expand Up @@ -114,11 +123,11 @@ function buildVideo(bidRequest) {

const optionalParams = {};
for (const param in OPTIONAL_VIDEO_PARAMS) {
if (bidRequestVideo[param] && OPTIONAL_VIDEO_PARAMS[param](bidRequestVideo[param])) {
if (bidRequestVideo[param] != null && OPTIONAL_VIDEO_PARAMS[param](bidRequestVideo[param])) {
optionalParams[param] = bidRequestVideo[param];
}
// remove invalid optional params from bidder specific overrides
if (videoBidderParams[param] && !OPTIONAL_VIDEO_PARAMS[param](videoBidderParams[param])) {
if (videoBidderParams[param] != null && !OPTIONAL_VIDEO_PARAMS[param](videoBidderParams[param])) {
delete videoBidderParams[param];
}
}
Expand All @@ -135,6 +144,20 @@ function buildVideo(bidRequest) {
optionalParams['context'] = context;
}

// Map Prebid.js adpod fields to ORTB 2.6 video fields
const adPodDurationSec = deepAccess(bidRequest, 'mediaTypes.video.adPodDurationSec');
if (adPodDurationSec && isInteger(adPodDurationSec) && adPodDurationSec > 0) {
optionalParams['poddur'] = adPodDurationSec;
}

const durationRangeSec = deepAccess(bidRequest, 'mediaTypes.video.durationRangeSec');
if (durationRangeSec && isArrayOfNums(durationRangeSec) && durationRangeSec.length > 0) {
const validDurations = durationRangeSec.filter(v => v > 0);
if (validDurations.length > 0) {
optionalParams['rqddurs'] = validDurations;
}
}

const videoObj = {
mimes,
w,
Expand Down Expand Up @@ -442,8 +465,9 @@ function buildRequest(validBidRequests, bidderRequest) {
return req;
}

function buildBid(bid, bidderRequest) {
function buildBid(bid, bidderRequest, seatbid) {
const originalBid = ((bidderRequest.bids) || []).find((b) => b.bidId === bid.impid);

let meta = {}

if (bid.ext && bid.ext.meta) {
Expand All @@ -454,27 +478,79 @@ function buildBid(bid, bidderRequest) {
meta.advertiserDomains = bid.adomain
}

// ORTB 2.6: Add category support
if (bid.cat && Array.isArray(bid.cat) && bid.cat.length > 0) {
meta.primaryCatId = bid.cat[0];
if (bid.cat.length > 1) {
meta.secondaryCatIds = bid.cat.slice(1);
}
}

// ORTB 2.6: Add seat from seatbid
if (seatbid && seatbid.seat) {
meta.seat = seatbid.seat;
}

// ORTB 2.6: Add creative attributes
if (bid.attr && Array.isArray(bid.attr)) {
meta.attr = bid.attr;
}

// Determine media type using multiple signals
let mediaType = 'banner';
if (bid.adm && bid.adm.includes('<VAST')) {

// 1. Check ORTB 2.6 mtype first (most reliable)
if (bid.mtype === 2) {
mediaType = 'video';
} else if (bid.mtype === 1) {
mediaType = 'banner';
// 2. Fall back to content detection (case-insensitive)
} else if (bid.adm && bid.adm.toLowerCase().includes('<vast') && !bid.adm.toLowerCase().includes('<script')) {
mediaType = 'video';
}

// TTL: Use bid.exp as upper bound if provided, otherwise use configTTL
const configTTL = config.getConfig('insticator.bidTTL') || BID_TTL;
const ttl = bid.exp && bid.exp > 0 ? Math.min(bid.exp, configTTL) : configTTL;

const bidResponse = {
requestId: bid.impid,
creativeId: bid.crid,
cpm: bid.price,
currency: 'USD',
netRevenue: true,
ttl: bid.exp || config.getConfig('insticator.bidTTL') || BID_TTL,
ttl: ttl,
width: bid.w,
height: bid.h,
mediaType: mediaType,
ad: bid.adm,
adUnitCode: originalBid.adUnitCode,
adUnitCode: originalBid?.adUnitCode,
...(Object.keys(meta).length > 0 ? {meta} : {})
};

// ORTB 2.6: Add deal ID
if (bid.dealid) {
bidResponse.dealId = bid.dealid;
}

// ORTB 2.6: Add billing URL for billing notification
if (bid.burl) {
Copy link

@rohanInsticator rohanInsticator Jan 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lets add nurl too if prebidjs supports it.

bidResponse.burl = bid.burl;
}

// ORTB 2.6: Add notice URL for win notification
if (bid.nurl) {
bidResponse.nurl = bid.nurl;
}

if (mediaType === 'video') {
bidResponse.vastXml = bid.adm;

// ORTB 2.6: Add video duration for adpod support
if (bid.dur && isInteger(bid.dur) && bid.dur > 0) {
bidResponse.video = bidResponse.video || {};
bidResponse.video.durationSeconds = bid.dur;
}
}

// Inticator bid adaptor only returns `vastXml` for video bids. No VastUrl or videoCache.
Expand All @@ -493,7 +569,7 @@ function buildBid(bid, bidderRequest) {
}

function buildBidSet(seatbid, bidderRequest) {
return seatbid.bid.map((bid) => buildBid(bid, bidderRequest));
return seatbid.bid.map((bid) => buildBid(bid, bidderRequest, seatbid));
}

function validateSize(size) {
Expand Down Expand Up @@ -652,17 +728,27 @@ export const spec = {
if (deepAccess(validBidRequests[0], 'params.bid_endpoint_request_url')) {
endpointUrl = deepAccess(validBidRequests[0], 'params.bid_endpoint_request_url').replace(/^http:/, 'https:');
}

// Add publisherId as query parameter if present and non-empty
const publisherId = deepAccess(validBidRequests[0], 'params.publisherId');
if (publisherId && publisherId.trim() !== '') {
const urlObj = new URL(endpointUrl);
urlObj.searchParams.set('publisherId', publisherId);
endpointUrl = urlObj.toString();
}
}

if (validBidRequests.length > 0) {
const ortbRequest = buildRequest(validBidRequests, bidderRequest);

requests.push({
method: 'POST',
url: endpointUrl,
options: {
contentType: 'application/json',
withCredentials: true,
},
data: JSON.stringify(buildRequest(validBidRequests, bidderRequest)),
data: JSON.stringify(ortbRequest),
bidderRequest,
});
}
Expand All @@ -673,11 +759,22 @@ export const spec = {
interpretResponse: function (serverResponse, request) {
const bidderRequest = request.bidderRequest;
const body = serverResponse.body;
if (!body || body.id !== bidderRequest.bidderRequestId) {
logError('insticator: response id does not match bidderRequestId');

// Handle 204 No Content or empty response body (valid "no bid" scenario)
if (!body || !body.id) {
return [];
}

// Validate response ID matches request ID
if (body.id !== bidderRequest.bidderRequestId) {
logError('insticator: response id does not match bidderRequestId', {
responseId: body.id,
bidderRequestId: bidderRequest.bidderRequestId
});
return [];
}

// No seatbid means no bids (valid scenario)
if (!body.seatbid) {
return [];
}
Expand All @@ -686,7 +783,9 @@ export const spec = {
buildBidSet(seatbid, bidderRequest)
);

return bidsets.reduce((a, b) => a.concat(b), []);
const finalBids = bidsets.reduce((a, b) => a.concat(b), []);

return finalBids;
},

getUserSyncs: function (options, responses) {
Expand Down
Loading