Skip to content

Commit 49c78ea

Browse files
pritishmd-talenticasushant-dey-talentica
authored andcommitted
AdSmartx Bid Adapter : New Bidder Adapter (prebid#14559)
* RM-1476 : Prebid adapter for adsmartx * RM-1476 : Improved unit test coverage * RM-1476 : Updated bidder documentation description * RM-1476 : Review comments handled * RM-1476 : Minor changes * RM-1476 : Dedupe code for adapter risemediatech * Added only ssp_id for user sync flow * Fixed bugs * RM-1476 : handled all copilot review comments * RM-1476 : Handled copilot review comments * RM-1476 : Handled prebid js PR review comments * RM-1476 : Handled review comment to log a warning that risemediatech bid adapter is deprecated * RM-1476: Added a function to disable the adapter. * RM-1476 : Updated unit tests --------- Co-authored-by: Sushant Dey <sushant.dey@talentica.com>
1 parent 62c6bac commit 49c78ea

7 files changed

Lines changed: 1717 additions & 305 deletions

File tree

Lines changed: 263 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,263 @@
1+
import { BANNER, VIDEO } from '../../src/mediaTypes.js';
2+
import { ortbConverter } from '../ortbConverter/converter.js';
3+
import { deepAccess, logInfo, logWarn } from '../../src/utils.js';
4+
5+
const DEFAULT_CURRENCY = 'USD';
6+
const DEFAULT_TTL = 60;
7+
8+
/**
9+
* Get publisher user ID with priority:
10+
* 1. Bid params (sspUserId)
11+
* 2. ORTB2 first party data (ortb2.user.id)
12+
* @param {Object} bidParams - Bid parameters from first bid
13+
* @param {Object} bidderRequest - Bidder request object containing ortb2
14+
* @returns {string|null} Publisher user ID if found, null otherwise
15+
*/
16+
export function getPublisherUserId(bidParams, bidderRequest) {
17+
if (bidParams?.sspUserId) {
18+
logInfo('Using SSP user ID from bid params:', bidParams.sspUserId);
19+
return bidParams.sspUserId;
20+
}
21+
const ortb2UserId = deepAccess(bidderRequest, 'ortb2.user.id');
22+
if (ortb2UserId) {
23+
logInfo('Using SSP user ID from ORTB2 user.id:', ortb2UserId);
24+
return ortb2UserId;
25+
}
26+
logInfo('No SSP user ID found in bid params or ORTB2');
27+
return null;
28+
}
29+
30+
/**
31+
* Creates ORTB converter with shared imp/request logic.
32+
* @param {Object} config - { defaultCurrency, defaultTtl }
33+
* @returns {Object} ortbConverter instance
34+
*/
35+
export function createConverter(config = {}) {
36+
const currency = config.defaultCurrency ?? DEFAULT_CURRENCY;
37+
const ttl = config.defaultTtl ?? DEFAULT_TTL;
38+
39+
return ortbConverter({
40+
context: {
41+
netRevenue: true,
42+
ttl,
43+
currency,
44+
},
45+
imp(buildImp, bidRequest, context) {
46+
logInfo('Building impression object for bidRequest:', bidRequest);
47+
const imp = buildImp(bidRequest, context);
48+
const { mediaTypes } = bidRequest;
49+
if (bidRequest.params?.bidfloor) {
50+
logInfo('Setting bid floor for impression:', bidRequest.params.bidfloor);
51+
imp.bidfloor = bidRequest.params.bidfloor;
52+
}
53+
if (mediaTypes[BANNER]) {
54+
logInfo('Adding banner media type to impression:', mediaTypes[BANNER]);
55+
imp.banner = { ...(imp.banner || {}), format: mediaTypes[BANNER].sizes.map(([w, h]) => ({ w, h })) };
56+
} else if (mediaTypes[VIDEO]) {
57+
logInfo('Adding video media type to impression:', mediaTypes[VIDEO]);
58+
imp.video = { ...(imp.video || {}), ...mediaTypes[VIDEO] };
59+
}
60+
return imp;
61+
},
62+
request(buildRequest, imps, bidderRequest, context) {
63+
logInfo('Building server request with impressions:', imps);
64+
const request = buildRequest(imps, bidderRequest, context);
65+
request.cur = [currency];
66+
request.tmax = bidderRequest.timeout;
67+
request.test = bidderRequest.test || 0;
68+
69+
if (Array.isArray(bidderRequest.bids)) {
70+
const hasTestMode = bidderRequest.bids.some(bid => bid.params?.testMode === 1);
71+
if (hasTestMode) {
72+
request.ext = request.ext || {};
73+
request.ext.test = 1;
74+
logInfo('Test mode detected in bid params, setting test flag in request:', request.ext.test);
75+
}
76+
const sspIdBid = bidderRequest.bids.find(bid => bid.params?.sspId);
77+
if (sspIdBid) {
78+
request.ext = request.ext || {};
79+
request.ext.sspId = sspIdBid.params.sspId;
80+
logInfo('sspId detected in bid params, setting sspId in request:', request.ext.sspId);
81+
}
82+
const siteIdBid = bidderRequest.bids.find(bid => bid.params?.siteId);
83+
if (siteIdBid) {
84+
request.ext = request.ext || {};
85+
request.ext.siteId = siteIdBid.params.siteId;
86+
logInfo('siteId detected in bid params, setting siteId in request:', request.ext.siteId);
87+
}
88+
}
89+
90+
if (bidderRequest.gdprConsent || bidderRequest.uspConsent) {
91+
request.regs = request.regs || {};
92+
request.user = request.user || {};
93+
}
94+
if (bidderRequest.gdprConsent) {
95+
logInfo('Adding GDPR consent information to request:', bidderRequest.gdprConsent);
96+
request.regs.gdpr = bidderRequest.gdprConsent.gdprApplies ? 1 : 0;
97+
request.user.consent = bidderRequest.gdprConsent.consentString;
98+
}
99+
if (bidderRequest.uspConsent) {
100+
logInfo('Adding USP consent information to request:', bidderRequest.uspConsent);
101+
request.regs.ext = request.regs.ext || {};
102+
request.regs.ext.us_privacy = bidderRequest.uspConsent;
103+
}
104+
return request;
105+
},
106+
});
107+
}
108+
109+
/**
110+
* Validates the bid request (video mimes/sizes, etc.).
111+
* @param {Object} bid - The bid request object.
112+
* @returns {boolean} True if the bid request is valid.
113+
*/
114+
export function isBidRequestValid(bid) {
115+
logInfo('Validating bid request:', bid);
116+
const { mediaTypes } = bid;
117+
118+
if (mediaTypes?.[VIDEO]) {
119+
const video = mediaTypes[VIDEO];
120+
if (!video.mimes || !Array.isArray(video.mimes) || video.mimes.length === 0) {
121+
logWarn('Invalid video bid request: Missing or invalid mimes.');
122+
return false;
123+
}
124+
// w and h are optional; if provided they must be positive
125+
if (video.w != null && video.w <= 0) {
126+
logWarn('Invalid video bid request: Invalid width.');
127+
return false;
128+
}
129+
if (video.h != null && video.h <= 0) {
130+
logWarn('Invalid video bid request: Invalid height.');
131+
return false;
132+
}
133+
}
134+
return true;
135+
}
136+
137+
/**
138+
* Builds buildRequests function that uses the given converter and endpoint.
139+
* @param {Object} config - { converter, endpointUrl }
140+
* @returns {function(Array, Object): Object}
141+
*/
142+
export function createBuildRequests(config) {
143+
const { converter, endpointUrl } = config;
144+
145+
return function buildRequests(validBidRequests, bidderRequest) {
146+
logInfo('Building server request for valid bid requests:', validBidRequests);
147+
148+
const request = converter.toORTB({ bidRequests: validBidRequests, bidderRequest });
149+
logInfo('Converted to ORTB request:', request);
150+
return {
151+
method: 'POST',
152+
url: endpointUrl,
153+
data: request,
154+
options: { endpointCompression: true },
155+
};
156+
};
157+
}
158+
159+
/**
160+
* Interprets the server response and extracts bid information.
161+
* @param {Object} serverResponse - The response from the server.
162+
* @param {Object} request - The original request sent to the server.
163+
* @param {Object} config - { defaultCurrency, defaultTtl }
164+
* @returns {Array} Array of bid objects.
165+
*/
166+
export function interpretResponse(serverResponse, request, config = {}) {
167+
const defaultCurrency = config.defaultCurrency ?? DEFAULT_CURRENCY;
168+
const defaultTtl = config.defaultTtl ?? DEFAULT_TTL;
169+
170+
logInfo('Interpreting server response:', serverResponse);
171+
const bidResp = serverResponse?.body;
172+
if (!bidResp || !Array.isArray(bidResp.seatbid)) {
173+
logWarn('Server response is empty, invalid, or does not contain seatbid array.');
174+
return [];
175+
}
176+
177+
const responses = [];
178+
bidResp.seatbid.forEach(seatbid => {
179+
if (!Array.isArray(seatbid.bid) || seatbid.bid.length === 0) return;
180+
const bid = seatbid.bid[0];
181+
if (!bid.impid || bid.price == null) {
182+
logWarn('Skipping bid with missing impid or price, bidId:', bid.id);
183+
return;
184+
}
185+
logInfo('Processing bid response:', bid);
186+
const bidResponse = {
187+
requestId: bid.impid,
188+
cpm: bid.price,
189+
currency: bidResp.cur || defaultCurrency,
190+
width: bid.w,
191+
height: bid.h,
192+
ad: bid.adm,
193+
creativeId: bid.crid,
194+
netRevenue: true,
195+
ttl: defaultTtl,
196+
meta: { advertiserDomains: bid.adomain || [] },
197+
};
198+
199+
switch (bid.mtype) {
200+
case 1:
201+
bidResponse.mediaType = BANNER;
202+
break;
203+
case 2:
204+
bidResponse.mediaType = VIDEO;
205+
bidResponse.vastXml = bid.adm;
206+
break;
207+
default:
208+
if (bid.mtype != null) {
209+
logWarn('Unknown media type: ', bid.mtype, ' for bidId: ', bid.id);
210+
} else {
211+
logWarn('Bid response does not contain media type for bidId: ', bid.id);
212+
}
213+
bidResponse.mediaType = BANNER;
214+
break;
215+
}
216+
217+
if (bid.dealid) bidResponse.dealId = bid.dealid;
218+
logInfo('Interpreted response:', bidResponse, ' for bidId: ', bid.id);
219+
responses.push(bidResponse);
220+
});
221+
222+
logInfo('Interpreted bid responses:', responses);
223+
return responses;
224+
}
225+
226+
/**
227+
* Creates getUserSyncs function that builds sync URL with privacy params.
228+
* @param {string} syncUrl - Base sync URL (e.g. 'https://sync.adsmartx.com/sync')
229+
* @returns {function(Object, Array, Object, string, Object): Array}
230+
*/
231+
export function createGetUserSyncs(syncUrl) {
232+
return function getUserSyncs(syncOptions, serverResponses, gdprConsent, uspConsent, gppConsent) {
233+
logInfo('getUserSyncs called with options:', syncOptions);
234+
if (!syncOptions.iframeEnabled && !syncOptions.pixelEnabled) {
235+
logWarn('User sync disabled: neither iframe nor pixel is enabled');
236+
return [];
237+
}
238+
239+
const params = [];
240+
if (gdprConsent) {
241+
params.push('gdpr=' + (gdprConsent.gdprApplies ? 1 : 0));
242+
params.push('gdpr_consent=' + encodeURIComponent(gdprConsent.consentString || ''));
243+
}
244+
if (uspConsent) {
245+
params.push('us_privacy=' + encodeURIComponent(uspConsent));
246+
}
247+
if (gppConsent?.gppString && gppConsent?.applicableSections?.length) {
248+
params.push('gpp=' + encodeURIComponent(gppConsent.gppString));
249+
params.push('gpp_sid=' + encodeURIComponent(gppConsent.applicableSections.join(',')));
250+
}
251+
252+
params.push('ssp_id=630141');
253+
params.push('iframe_enabled=' + (syncOptions.iframeEnabled ? 'true' : 'false'));
254+
255+
const queryString = params.length ? '?' + params.join('&') : '';
256+
const syncs = [{
257+
type: syncOptions.iframeEnabled ? 'iframe' : 'image',
258+
url: syncUrl + queryString,
259+
}];
260+
logInfo('Returning user syncs, type:', syncs[0]?.type);
261+
return syncs;
262+
};
263+
}

modules/adsmartxBidAdapter.js

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { registerBidder } from '../src/adapters/bidderFactory.js';
2+
import { BANNER, VIDEO } from '../src/mediaTypes.js';
3+
import {
4+
createConverter,
5+
isBidRequestValid as validateBidRequest,
6+
createBuildRequests,
7+
interpretResponse as interpretResponseUtil,
8+
createGetUserSyncs,
9+
} from '../libraries/adsmartxUtils/bidderUtils.js';
10+
11+
const BIDDER_CODE = 'adsmartx';
12+
const ENDPOINT_URL = 'https://ads.adsmartx.com/ads/rtb/prebid/js';
13+
const SYNC_URL = 'https://sync.adsmartx.com/sync';
14+
const DEFAULT_CURRENCY = 'USD';
15+
const DEFAULT_TTL = 60;
16+
17+
const converter = createConverter({ defaultCurrency: DEFAULT_CURRENCY, defaultTtl: DEFAULT_TTL });
18+
19+
const isBidRequestValid = validateBidRequest;
20+
const buildRequests = createBuildRequests(
21+
{ converter, endpointUrl: ENDPOINT_URL }
22+
);
23+
const getUserSyncs = createGetUserSyncs(SYNC_URL);
24+
25+
const interpretResponse = (serverResponse, request) => {
26+
return interpretResponseUtil(serverResponse, request, {
27+
defaultCurrency: DEFAULT_CURRENCY,
28+
defaultTtl: DEFAULT_TTL,
29+
});
30+
};
31+
32+
export const spec = {
33+
code: BIDDER_CODE,
34+
// TODO: set gvlid once confirmed with AI Digital / AdSmartX team
35+
gvlid: undefined,
36+
supportedMediaTypes: [BANNER, VIDEO],
37+
isBidRequestValid,
38+
buildRequests,
39+
interpretResponse,
40+
getUserSyncs,
41+
};
42+
43+
registerBidder(spec);

modules/adsmartxBidAdapter.md

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
# Overview
2+
3+
Module Name : AdSmartX Bidder Adapter
4+
Module Type : Bid Adapter
5+
Maintainer : prebid@aidigital.com
6+
7+
# Description
8+
Connects to AdSmartX Exchange for bids
9+
AdSmartX supports Display & Video(Instream) currently.
10+
11+
This adapter is maintained by Smart Exchange, the legal entity behind this implementation. Our official domain is [AI Digital](https://www.aidigital.com/).
12+
# Sample Ad Unit : Banner
13+
```
14+
var adUnits = [
15+
{
16+
code: 'test-banner-div',
17+
mediaTypes: {
18+
banner: {
19+
sizes:[
20+
[320,50]
21+
]
22+
}
23+
},
24+
bids:[
25+
{
26+
bidder: 'adsmartx',
27+
params: {
28+
bidfloor: 0.001,
29+
testMode: 1,
30+
sspId: 123456,
31+
siteId: 987654,
32+
sspUserId: 'u1234'
33+
}
34+
}
35+
]
36+
}
37+
]
38+
```
39+
40+
# Sample Ad Unit : Video
41+
```
42+
var videoAdUnit = [
43+
{
44+
code: 'adsmartx',
45+
mediaTypes: {
46+
video: {
47+
playerSize: [640, 480], // required
48+
context: 'instream',
49+
mimes: ['video/mp4','video/webm'],
50+
minduration: 5,
51+
maxduration: 30,
52+
startdelay: 30,
53+
maxseq: 2,
54+
poddur: 30,
55+
protocols: [1,3,4],
56+
}
57+
},
58+
bids:[
59+
{
60+
bidder: 'adsmartx',
61+
params: {
62+
bidfloor: 0.001,
63+
testMode: 1,
64+
sspId: 123456,
65+
siteId: 987654,
66+
sspUserId: 'u1234'
67+
}
68+
}
69+
]
70+
}
71+
]
72+
```

0 commit comments

Comments
 (0)