Skip to content

Commit e9991ef

Browse files
theo-stvtheo_
authored andcommitted
Stv Bid Adapter : initial adapter release (prebid#9533)
* initial commit * adapted buildRequests function * refinement pfilter and bcat * refinement * adapted tests for isBidRequestValid,buildRequests * adaptations for test * finished building stvBidAdapter.js * finished: ran tests, coverage 99% * update: rename w->srw, h->srh * adapt stvBidAdapter.md * remove dspx from stv adapters * some changes (missing: getUserSyncs, but is the same as in radsBidAdapter) * added checks in getUserSyncs; ran tests --------- Co-authored-by: theo_ <theo_@IDEA3>
1 parent d9d5230 commit e9991ef

File tree

3 files changed

+794
-0
lines changed

3 files changed

+794
-0
lines changed

modules/stvBidAdapter.js

Lines changed: 330 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,330 @@
1+
import { deepAccess } from '../src/utils.js';
2+
import { config } from '../src/config.js';
3+
import { registerBidder } from '../src/adapters/bidderFactory.js';
4+
import { BANNER, VIDEO } from '../src/mediaTypes.js';
5+
import {includes} from '../src/polyfill.js';
6+
7+
const BIDDER_CODE = 'stv';
8+
const ENDPOINT_URL = 'https://ads.smartstream.tv/r/';
9+
const ENDPOINT_URL_DEV = 'https://ads.smartstream.tv/r/';
10+
const GVLID = 134;
11+
const VIDEO_ORTB_PARAMS = {
12+
'minduration': 'min_duration',
13+
'maxduration': 'max_duration',
14+
'maxbitrate': 'max_bitrate',
15+
'api': 'api',
16+
};
17+
18+
export const spec = {
19+
code: BIDDER_CODE,
20+
gvlid: GVLID,
21+
aliases: [],
22+
supportedMediaTypes: [BANNER, VIDEO],
23+
isBidRequestValid: function(bid) {
24+
return !!(bid.params.placement);
25+
},
26+
buildRequests: function(validBidRequests, bidderRequest) {
27+
return validBidRequests.map(bidRequest => {
28+
const params = bidRequest.params;
29+
30+
const placementId = params.placement;
31+
const rnd = Math.floor(Math.random() * 99999999999);
32+
const referrer = bidderRequest.refererInfo.page;
33+
const bidId = bidRequest.bidId;
34+
const isDev = params.devMode || false;
35+
const pbcode = bidRequest.adUnitCode || false; // div id
36+
37+
let endpoint = isDev ? ENDPOINT_URL_DEV : ENDPOINT_URL;
38+
39+
let mediaTypesInfo = getMediaTypesInfo(bidRequest);
40+
let type = isBannerRequest(bidRequest) ? BANNER : VIDEO;
41+
let sizes = mediaTypesInfo[type];
42+
43+
let payload = {
44+
_f: 'vast2',
45+
alternative: 'prebid_js',
46+
_ps: placementId,
47+
srw: sizes ? sizes[0].width : 0,
48+
srh: sizes ? sizes[0].height : 0,
49+
idt: 100,
50+
rnd: rnd,
51+
ref: referrer,
52+
bid_id: bidId,
53+
pbver: '$prebid.version$',
54+
};
55+
if (!isVideoRequest(bidRequest)) {
56+
payload._f = 'html';
57+
}
58+
59+
payload.pfilter = { ...params };
60+
delete payload.pfilter.placement;
61+
if (params.bcat !== undefined) { delete payload.pfilter.bcat; }
62+
if (params.dvt !== undefined) { delete payload.pfilter.dvt; }
63+
if (params.devMode !== undefined) { delete payload.pfilter.devMode; }
64+
65+
if (payload.pfilter === undefined || !payload.pfilter.floorprice) {
66+
let bidFloor = getBidFloor(bidRequest);
67+
if (bidFloor > 0) {
68+
if (payload.pfilter !== undefined) {
69+
payload.pfilter.floorprice = bidFloor;
70+
} else {
71+
payload.pfilter = { 'floorprice': bidFloor };
72+
}
73+
// payload.bidFloor = bidFloor;
74+
}
75+
}
76+
77+
if (mediaTypesInfo[VIDEO] !== undefined) {
78+
let videoParams = deepAccess(bidRequest, 'mediaTypes.video');
79+
Object.keys(videoParams)
80+
.filter(key => includes(Object.keys(VIDEO_ORTB_PARAMS), key) && params[VIDEO_ORTB_PARAMS[key]] === undefined)
81+
.forEach(key => payload.pfilter[VIDEO_ORTB_PARAMS[key]] = videoParams[key]);
82+
}
83+
if (Object.keys(payload.pfilter).length == 0) { delete payload.pfilter }
84+
85+
if (bidderRequest && bidderRequest.gdprConsent) {
86+
payload.gdpr_consent = bidderRequest.gdprConsent.consentString;
87+
payload.gdpr = bidderRequest.gdprConsent.gdprApplies;
88+
}
89+
90+
if (params.bcat !== undefined) {
91+
payload.bcat = deepAccess(bidderRequest.ortb2Imp, 'bcat') || params.bcat;
92+
}
93+
if (params.dvt !== undefined) {
94+
payload.dvt = params.dvt;
95+
}
96+
if (isDev) {
97+
payload.prebidDevMode = 1;
98+
}
99+
100+
if (pbcode) {
101+
payload.pbcode = pbcode;
102+
}
103+
104+
payload.media_types = convertMediaInfoForRequest(mediaTypesInfo);
105+
106+
return {
107+
method: 'GET',
108+
url: endpoint,
109+
data: objectToQueryString(payload),
110+
};
111+
});
112+
},
113+
interpretResponse: function(serverResponse, bidRequest) {
114+
const bidResponses = [];
115+
const response = serverResponse.body;
116+
const crid = response.crid || 0;
117+
const cpm = response.cpm / 1000000 || 0;
118+
if (cpm !== 0 && crid !== 0) {
119+
const dealId = response.dealid || '';
120+
const currency = response.currency || 'EUR';
121+
const netRevenue = (response.netRevenue === undefined) ? true : response.netRevenue;
122+
const bidResponse = {
123+
requestId: response.bid_id,
124+
cpm: cpm,
125+
width: response.width,
126+
height: response.height,
127+
creativeId: crid,
128+
dealId: dealId,
129+
currency: currency,
130+
netRevenue: netRevenue,
131+
ttl: config.getConfig('_bidderTimeout'),
132+
meta: {
133+
advertiserDomains: response.adomain || []
134+
}
135+
};
136+
if (response.vastXml) {
137+
bidResponse.vastXml = response.vastXml;
138+
bidResponse.mediaType = 'video';
139+
} else {
140+
bidResponse.ad = response.adTag;
141+
}
142+
143+
bidResponses.push(bidResponse);
144+
}
145+
return bidResponses;
146+
},
147+
getUserSyncs: function(syncOptions, serverResponses, gdprConsent, uspConsent) {
148+
if (!serverResponses || serverResponses.length === 0) {
149+
return [];
150+
}
151+
152+
const syncs = []
153+
154+
let gdprParams = '';
155+
if (gdprConsent) {
156+
if ('gdprApplies' in gdprConsent && typeof gdprConsent.gdprApplies === 'boolean') {
157+
gdprParams = `gdpr=${Number(gdprConsent.gdprApplies)}&gdpr_consent=${gdprConsent.consentString}`;
158+
} else {
159+
gdprParams = `gdpr_consent=${gdprConsent.consentString}`;
160+
}
161+
}
162+
163+
if (serverResponses.length > 0 && serverResponses[0].body !== undefined &&
164+
serverResponses[0].body.userSync !== undefined && serverResponses[0].body.userSync.iframeUrl !== undefined &&
165+
serverResponses[0].body.userSync.iframeUrl.length > 0) {
166+
if (syncOptions.iframeEnabled) {
167+
serverResponses[0].body.userSync.iframeUrl.forEach((url) => syncs.push({
168+
type: 'iframe',
169+
url: appendToUrl(url, gdprParams)
170+
}));
171+
}
172+
if (syncOptions.pixelEnabled) {
173+
serverResponses[0].body.userSync.imageUrl.forEach((url) => syncs.push({
174+
type: 'image',
175+
url: appendToUrl(url, gdprParams)
176+
}));
177+
}
178+
}
179+
return syncs;
180+
}
181+
}
182+
183+
function appendToUrl(url, what) {
184+
if (!what) {
185+
return url;
186+
}
187+
return url + (url.indexOf('?') !== -1 ? '&' : '?') + what;
188+
}
189+
190+
function objectToQueryString(obj, prefix) {
191+
let str = [];
192+
let p;
193+
for (p in obj) {
194+
if (obj.hasOwnProperty(p)) {
195+
let k = prefix ? prefix + '[' + p + ']' : p;
196+
let v = obj[p];
197+
str.push((v !== null && typeof v === 'object')
198+
? objectToQueryString(v, k)
199+
: encodeURIComponent(k) + '=' + encodeURIComponent(v));
200+
}
201+
}
202+
return str.join('&');
203+
}
204+
205+
/**
206+
* Check if it's a banner bid request
207+
*
208+
* @param {BidRequest} bid - Bid request generated from ad slots
209+
* @returns {boolean} True if it's a banner bid
210+
*/
211+
function isBannerRequest(bid) {
212+
return bid.mediaType === 'banner' || !!deepAccess(bid, 'mediaTypes.banner') || !isVideoRequest(bid);
213+
}
214+
215+
/**
216+
* Check if it's a video bid request
217+
*
218+
* @param {BidRequest} bid - Bid request generated from ad slots
219+
* @returns {boolean} True if it's a video bid
220+
*/
221+
function isVideoRequest(bid) {
222+
return bid.mediaType === 'video' || !!deepAccess(bid, 'mediaTypes.video');
223+
}
224+
225+
/**
226+
* Get video sizes
227+
*
228+
* @param {BidRequest} bid - Bid request generated from ad slots
229+
* @returns {object} True if it's a video bid
230+
*/
231+
function getVideoSizes(bid) {
232+
return parseSizes(deepAccess(bid, 'mediaTypes.video.playerSize') || bid.sizes);
233+
}
234+
235+
/**
236+
* Get banner sizes
237+
*
238+
* @param {BidRequest} bid - Bid request generated from ad slots
239+
* @returns {object} True if it's a video bid
240+
*/
241+
function getBannerSizes(bid) {
242+
return parseSizes(deepAccess(bid, 'mediaTypes.banner.sizes') || bid.sizes);
243+
}
244+
245+
/**
246+
* Parse size
247+
* @param sizes
248+
* @returns {width: number, h: height}
249+
*/
250+
function parseSize(size) {
251+
let sizeObj = {}
252+
sizeObj.width = parseInt(size[0], 10);
253+
sizeObj.height = parseInt(size[1], 10);
254+
return sizeObj;
255+
}
256+
257+
/**
258+
* Parse sizes
259+
* @param sizes
260+
* @returns {{width: number , height: number }[]}
261+
*/
262+
function parseSizes(sizes) {
263+
if (Array.isArray(sizes[0])) { // is there several sizes ? (ie. [[728,90],[200,300]])
264+
return sizes.map(size => parseSize(size));
265+
}
266+
return [parseSize(sizes)]; // or a single one ? (ie. [728,90])
267+
}
268+
269+
/**
270+
* Get MediaInfo object for server request
271+
*
272+
* @param mediaTypesInfo
273+
* @returns {*}
274+
*/
275+
function convertMediaInfoForRequest(mediaTypesInfo) {
276+
let requestData = {};
277+
Object.keys(mediaTypesInfo).forEach(mediaType => {
278+
requestData[mediaType] = mediaTypesInfo[mediaType].map(size => {
279+
return size.width + 'x' + size.height;
280+
}).join(',');
281+
});
282+
return requestData;
283+
}
284+
285+
/**
286+
* Get Bid Floor
287+
* @param bid
288+
* @returns {number|*}
289+
*/
290+
function getBidFloor(bid) {
291+
if (typeof bid.getFloor !== 'function') {
292+
return deepAccess(bid, 'params.bidfloor', 0);
293+
}
294+
295+
try {
296+
const bidFloor = bid.getFloor({
297+
currency: 'EUR',
298+
mediaType: '*',
299+
size: '*',
300+
});
301+
return bidFloor.floor;
302+
} catch (_) {
303+
return 0
304+
}
305+
}
306+
307+
/**
308+
* Get media types info
309+
*
310+
* @param bid
311+
*/
312+
function getMediaTypesInfo(bid) {
313+
let mediaTypesInfo = {};
314+
315+
if (bid.mediaTypes) {
316+
Object.keys(bid.mediaTypes).forEach(mediaType => {
317+
if (mediaType === BANNER) {
318+
mediaTypesInfo[mediaType] = getBannerSizes(bid);
319+
}
320+
if (mediaType === VIDEO) {
321+
mediaTypesInfo[mediaType] = getVideoSizes(bid);
322+
}
323+
});
324+
} else {
325+
mediaTypesInfo[BANNER] = getBannerSizes(bid);
326+
}
327+
return mediaTypesInfo;
328+
}
329+
330+
registerBidder(spec);

modules/stvBidAdapter.md

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
# Overview
2+
3+
```
4+
Module Name: STV/Smartstream Bidder Adapter
5+
Module Type: Bidder Adapter
6+
Maintainer: [email protected]
7+
```
8+
9+
# Description
10+
11+
STV/Smartstream adapter for Prebid.
12+
13+
# Test Parameters
14+
```
15+
var adUnits = [
16+
{
17+
code: 'test-div',
18+
mediaTypes: {
19+
banner: {
20+
sizes: [
21+
[300, 250],
22+
[300, 600],
23+
]
24+
}
25+
},
26+
bids: [
27+
{
28+
bidder: "stv",
29+
params: {
30+
placement: '101', // [required] info available from your contact with Smartstream team
31+
/* // [optional params]
32+
bcat: "IAB2,IAB4", // [optional] list of blocked advertiser categories (IAB), comma separated
33+
*/
34+
}
35+
}
36+
]
37+
},
38+
{
39+
code: 'video1',
40+
mediaTypes: {
41+
video: {
42+
playerSize: [640, 480],
43+
context: 'instream'
44+
}
45+
},
46+
bids: [{
47+
bidder: 'stv',
48+
params: {
49+
placement: '106',
50+
/* // [optional params]
51+
bcat: "IAB2,IAB4", // [optional] list of blocked advertiser categories (IAB), comma separated
52+
floorprice: 1000000, // input min_cpm_micros, CPM in EUR * 1000000
53+
max_duration: 60, // in seconds
54+
min_duration: 5, // in seconds
55+
max_bitrate: 600,
56+
api: [1,2], // https://github.com/InteractiveAdvertisingBureau/AdCOM/blob/master/AdCOM%20v1.0%20FINAL.md#list--api-frameworks-
57+
*/
58+
}
59+
}]
60+
}
61+
];
62+
```

0 commit comments

Comments
 (0)