From 3b7e0b621081ee549ac576c85b610db6e36be608 Mon Sep 17 00:00:00 2001 From: "Prebid Start.io" Date: Wed, 16 Apr 2025 13:47:43 +0300 Subject: [PATCH 1/3] Implementation of the Start.io adapter --- modules/startioBidAdapter.js | 100 +++++++ modules/startioBidAdapter.md | 101 +++++++ test/spec/modules/startioBidAdapter_spec.js | 281 ++++++++++++++++++++ 3 files changed, 482 insertions(+) create mode 100644 modules/startioBidAdapter.js create mode 100644 modules/startioBidAdapter.md create mode 100644 test/spec/modules/startioBidAdapter_spec.js diff --git a/modules/startioBidAdapter.js b/modules/startioBidAdapter.js new file mode 100644 index 00000000000..08339eeec21 --- /dev/null +++ b/modules/startioBidAdapter.js @@ -0,0 +1,100 @@ +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { BANNER, VIDEO, NATIVE } from '../src/mediaTypes.js'; +import { deepAccess, deepSetValue, logError, triggerNurlWithCpm } from '../src/utils.js'; +import { ortbConverter } from '../libraries/ortbConverter/converter.js' + +const BIDDER_CODE = 'startio'; +const METHOD = 'POST'; +const GVLID = 1216; +const ENDPOINT_URL = `http://pbc-rtb.startappnetwork.com/1.3/2.5/getbid?account=pbc`; + +const converter = ortbConverter({ + imp(buildImp, bidRequest, context) { + const imp = buildImp(bidRequest, context); + + if (imp['banner']) { + deepSetValue(imp, 'banner.w', deepAccess(imp, 'banner.format.0.w')); + deepSetValue(imp, 'banner.h', deepAccess(imp, 'banner.format.0.h')); + } + + return imp; + }, + request(buildRequest, imps, bidderRequest, context) { + const request = buildRequest(imps, bidderRequest, context); + const publisherId = deepAccess(bidderRequest, 'bids.0.params.publisherId'); + + if (request['site']) { + deepSetValue(request, 'site.publisher.id', publisherId); + } else { + deepSetValue(request, 'app.publisher.id', publisherId); + } + deepSetValue(request, 'ext.prebid', {}); + + return request; + }, + bidResponse(buildBidResponse, bid, context) { + const isValidBidType = deepAccess(bid, 'ext.prebid.type') === deepAccess(context, 'mediaType'); + + if (context.mediaType === NATIVE) { + const ortb = JSON.parse(bid.adm); + bid.adm = ortb.native; + } + + if (isValidBidType) { + return buildBidResponse(bid, context); + } + + logError('Bid type is incorrect for bid: ', bid['id']) + }, + context: { + netRevenue: true, + ttl: 30 + } +}); + +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: [VIDEO, BANNER, NATIVE], + gvlid: GVLID, + isBidRequestValid: (bid) => !!bid, + + buildRequests: (bidRequests, bidderRequest) => { + return bidRequests.map((bidRequest) => { + const mediaType = Object.keys(bidRequest.mediaTypes || {})[0] || BANNER; + const data = converter.toORTB({ bidRequests: [bidRequest], bidderRequest, context: { mediaType } }); + + return { + method: METHOD, + url: ENDPOINT_URL, + options: { + contentType: 'application/json', + withCredentials: false, + crossOrigin: true + }, + data: data, + }; + }); + }, + + interpretResponse: ({ body }, req) => { + if (!body || !body.seatbid || body.seatbid.length === 0) { + return []; + } + return converter.fromORTB({ + response: body, + request: req.data + }); + }, + + onTimeout: (data) => { }, + + onBidWon: (bid) => { + if (bid.nurl) { + triggerNurlWithCpm(bid, bid.cpm) + } + }, + + onSetTargeting: (bid) => { }, +}; + +registerBidder(spec); diff --git a/modules/startioBidAdapter.md b/modules/startioBidAdapter.md new file mode 100644 index 00000000000..172af1aeb4e --- /dev/null +++ b/modules/startioBidAdapter.md @@ -0,0 +1,101 @@ +# Overview + +``` +Module Name: Start.io Bidder Adapter +Module Type: Bidder Adapter +Maintainer: prebid@start.io +``` + +# Description + +The Start.io Bid Adapter enables publishers to integrate with Start.io's demand sources for banner, video and native ad formats. The adapter supports OpenRTB standards and processes bid requests efficiently using the Prebid.js framework. + +# Test Parameters +``` +var adUnits = [ + { + code: 'test-div', + mediaTypes: { + banner: { + sizes: [[300,250], [728,90]] + } + }, + bids: [ + { + bidder: 'startio', + params: { + // REQUIRED - Publisher Account ID + accountId: 'your-account-id', + + // OPTIONAL - Enable test ads + testAdsEnabled: true + } + } + ] + } +]; +``` + +# Sample Instream Video Ad Unit: For Publishers +``` +var videoAdUnits = [ + { + code: 'test-div-video', + mediaTypes: { + video: { + context: 'instream', + placement: 1, + playerSize: [640, 360], + mimes: ['video/mp4'], + protocols: [2, 3, 5, 6], + api: [2], + maxduration: 30, + linearity: 1, + playbackmethod: [2] + } + }, + bids: [ + { + bidder: 'startio', + params: { + accountId: 'your-account-id', + testAdsEnabled: true + } + } + ] + } +]; +``` + +# Sample Native Ad Unit: For Publishers +``` +var nativeAdUnits = [ + { + code: 'test-div-native', + mediaTypes: { + native: { + title: { required: true, len: 80 }, + body: { required: true }, + image: { required: true, sizes: [150, 150] }, + icon: { required: false, sizes: [50, 50] }, + sponsoredBy: { required: true } + } + }, + bids: [ + { + bidder: 'startio', + params: { + accountId: 'your-account-id', + testAdsEnabled: true + } + } + ] + } +]; +``` + +# Additional Notes +- The adapter processes requests via OpenRTB 2.5 standards. +- Ensure that the `accountId` parameter is set correctly for your integration. +- Test ads can be enabled using `testAdsEnabled: true` during development. +- The adapter supports multiple ad formats, allowing publishers to serve banners, native ads and instream video ads seamlessly. diff --git a/test/spec/modules/startioBidAdapter_spec.js b/test/spec/modules/startioBidAdapter_spec.js new file mode 100644 index 00000000000..0bde0003008 --- /dev/null +++ b/test/spec/modules/startioBidAdapter_spec.js @@ -0,0 +1,281 @@ +import { expect } from 'chai'; +import { spec } from 'modules/startioBidAdapter.js'; +import { BANNER, VIDEO, NATIVE } from 'src/mediaTypes.js'; + +const DEFAULT_REQUEST_DATA = { + adUnitCode: 'test-div', + auctionId: 'b06c5141-fe8f-4cdf-9d7d-54415490a917', + bidId: '32d4d86b4f22ed', + bidder: 'startio', + bidderRequestId: '1bbb7854dfa0d8', + mediaTypes: { + banner: { + sizes: [ + [300, 250], + [300, 600] + ] + } + }, + params: {}, + src: 'client', + transactionId: 'db739693-9b4a-4669-9945-8eab938783cc' +} + +const VALID_MEDIA_TYPES_REQUESTS = { + [BANNER]: [{ + ...DEFAULT_REQUEST_DATA, + mediaTypes: { + [BANNER]: { + sizes: [ + [300, 250], + [300, 600] + ] + } + }, + }], + [VIDEO]: [{ + ...DEFAULT_REQUEST_DATA, + mediaTypes: { + video: { + minduration: 3, + maxduration: 43, + playerSize: [640, 480], + mimes: ['video/mp4'], + protocols: [2] + } + }, + }], + [NATIVE]: [{ + ...DEFAULT_REQUEST_DATA, + mediaTypes: { + [NATIVE]: { + title: { required: true, len: 200 }, + image: { required: true, sizes: [150, 50] }, + } + }, + nativeOrtbRequest: { + assets: [ + { required: 1, title: { len: 200 } }, + { required: 1, img: { type: 3, w: 150, h: 50 } }, + ] + }, + }] +} + +const VALID_BIDDER_REQUEST = { + auctionId: '19c97f22-5bd1-4b16-a128-80f75fb0a8a0', + bidderCode: 'startio', + bidderRequestId: '1bbb7854dfa0d8', + bids: [ + { + params: {}, + } + ], + refererInfo: { + page: 'test-page', + domain: 'test-domain', + ref: 'test-referer' + }, +} + +const DEFAULT_BID_RESPONSE_DATA = { + 'id': '29596384-e502-4d3c-a47d-4f16b16bd554', + 'impid': '32d4d86b4f22ed', + 'price': 0.18417903447819028, + 'adid': '2:64:162:1001', + 'adomain': [ + 'start.io' + ], + 'nurl': 'https://start.io/v1', + 'lurl': 'https://start.io/v1', + 'iurl': 'https://start.io/v1', + 'cid': '1982494692188097775', + 'crid': '5889732975267688811', + 'cat': ['IAB1-1', 'IAB1-6'], + 'w': 300, + 'h': 250, + 'mtype': 1, +}; + +const SERVER_RESPONSE_BANNER = { + 'id': '5d997535-e900-4a6b-9cb7-737e402d5cfa', + 'seatbid': [ + { + 'bid': [ + { + ...DEFAULT_BID_RESPONSE_DATA, + 'adm': 'banner.img', + 'ext': { + 'duration': 0, + 'prebid': { + 'type': BANNER + } + } + } + ], + 'seat': 'start.io', + 'group': 0 + } + ], + 'cur': 'USD' +} + +const SERVER_RESPONSE_VIDEO = { + 'id': '8cd85aed-25a6-4db0-ad98-4a3af1f7601c', + 'seatbid': [ + { + 'bid': [ + { + ...DEFAULT_BID_RESPONSE_DATA, + 'adm': '', + 'ext': { + 'duration': 0, + 'prebid': { + 'type': VIDEO + } + } + } + ], + 'seat': 'start.io', + 'group': 0 + } + ], + 'cur': 'USD' +} + +const SERVER_RESPONSE_NATIVE = { + 'id': '29667448-5659-42bb-abcf-dc973f98eae1', + 'seatbid': [ + { + 'bid': [ + { + ...DEFAULT_BID_RESPONSE_DATA, + 'adm': '{"native":{"assets":[{"id":0,"title":{"len":90,"text":"Title"}}, {"id":1,"img":{"w":320,"h":250,"url":"https://img.image.com/product/image.jpg"}}]}}', + 'ext': { + 'duration': 0, + 'prebid': { + 'type': NATIVE + } + } + } + ], + 'seat': 'start.io', + 'group': 0 + } + ], + 'cur': 'USD' +} + + +describe('Prebid Adapter: Startio', function () { + describe('code', function () { + it('should return a bidder code of startio', function () { + expect(spec.code).to.eql('startio'); + }); + }); + + describe('isBidRequestValid', function () { + it('should return true for bid request', function () { + const bidRequest = { + bidder: 'startio', + }; + expect(spec.isBidRequestValid(bidRequest)).to.eql(true); + }); + }); + + describe('buildRequests', function () { + it('should build request for banner media type', function () { + const bidRequest = VALID_MEDIA_TYPES_REQUESTS[BANNER][0]; + const bidderRequest = { + refererInfo: { referer: 'https://example.com' }, + }; + + const requests = spec.buildRequests([bidRequest], bidderRequest); + + expect(requests).to.have.lengthOf(1); + const request = requests[0]; + expect(request.method).to.equal('POST'); + expect(request.data).to.have.property('imp'); + expect(request.data.imp[0].banner.w).to.equal(300); + expect(request.data.imp[0].banner.h).to.equal(250); + }); + if (FEATURES.VIDEO) { + it('should build request for video media type', function () { + const bidRequest = VALID_MEDIA_TYPES_REQUESTS[VIDEO][0]; + const bidderRequest = { + refererInfo: { referer: 'https://example.com' }, + }; + + const requests = spec.buildRequests([bidRequest], bidderRequest); + + expect(requests).to.have.lengthOf(1); + const request = requests[0]; + + expect(request.data.imp[0].video).to.exist; + expect(request.data.imp[0].video.minduration).to.equal(3); + expect(request.data.imp[0].video.maxduration).to.equal(43); + }); + } + + if (FEATURES.NATIVE) { + it('should build request for native media type', function () { + const bidRequest = VALID_MEDIA_TYPES_REQUESTS[NATIVE][0]; + const bidderRequest = { + refererInfo: { referer: 'https://example.com' }, + }; + + const requests = spec.buildRequests([bidRequest], bidderRequest); + + expect(requests).to.have.lengthOf(1); + const request = requests[0]; + + expect(request.data.imp[0].native).to.exist; + }); + } + }); + + describe('interpretResponse', function () { + it('should return a valid bid array with a banner bid', () => { + const requests = spec.buildRequests(VALID_MEDIA_TYPES_REQUESTS[BANNER], VALID_BIDDER_REQUEST) + const { data } = requests[0]; + const bids = spec.interpretResponse({ body: SERVER_RESPONSE_BANNER }, { data }).bids; + + expect(bids).to.be.a('array').that.has.lengthOf(1) + bids.forEach(value => { + expect(value).to.be.a('object').that.has.all.keys( + 'ad', 'cpm', 'creativeId', 'currency', 'height', 'mediaType', 'meta', 'netRevenue', 'requestId', 'ttl', 'width', 'seatBidId', 'creative_id' + ) + }) + }); + + if (FEATURES.VIDEO) { + it('should return a valid bid array with a video bid', () => { + const requests = spec.buildRequests(VALID_MEDIA_TYPES_REQUESTS[VIDEO], VALID_BIDDER_REQUEST); + const { data } = requests[0]; + const bids = spec.interpretResponse({ body: SERVER_RESPONSE_VIDEO }, { data }).bids + expect(bids).to.be.a('array').that.has.lengthOf(1) + bids.forEach(value => { + expect(value).to.be.a('object').that.has.all.keys( + 'vastUrl', 'vastXml', 'playerHeight', 'playerWidth', 'cpm', 'creativeId', 'currency', 'height', 'mediaType', 'meta', 'netRevenue', 'requestId', 'ttl', 'width', 'seatBidId', 'creative_id' + ) + }) + }); + } + + if (FEATURES.NATIVE) { + it('should return a valid bid array with a native bid', () => { + const requests = spec.buildRequests(VALID_MEDIA_TYPES_REQUESTS[NATIVE], VALID_BIDDER_REQUEST); + const { data } = requests[0]; + const bids = spec.interpretResponse({ body: SERVER_RESPONSE_NATIVE }, { data }).bids + expect(bids).to.be.a('array').that.has.lengthOf(1) + bids.forEach(value => { + expect(value).to.be.a('object').that.has.all.keys( + 'native', 'cpm', 'creativeId', 'currency', 'height', 'mediaType', 'meta', 'netRevenue', 'requestId', 'ttl', 'width', 'seatBidId', 'creative_id' + ) + }) + }); + } + }); + + +}); From 122eba4051848efc03bf3cd9adc3d48d778d3aa1 Mon Sep 17 00:00:00 2001 From: "Prebid Start.io" Date: Thu, 15 May 2025 13:18:15 +0300 Subject: [PATCH 2/3] Refactor based on review suggestions --- modules/startioBidAdapter.js | 32 +++++++++++++-------- test/spec/modules/startioBidAdapter_spec.js | 14 +++++++-- 2 files changed, 32 insertions(+), 14 deletions(-) diff --git a/modules/startioBidAdapter.js b/modules/startioBidAdapter.js index 08339eeec21..910e8d0336d 100644 --- a/modules/startioBidAdapter.js +++ b/modules/startioBidAdapter.js @@ -1,7 +1,8 @@ import { registerBidder } from '../src/adapters/bidderFactory.js'; import { BANNER, VIDEO, NATIVE } from '../src/mediaTypes.js'; -import { deepAccess, deepSetValue, logError, triggerNurlWithCpm } from '../src/utils.js'; +import { deepAccess, deepSetValue, logError } from '../src/utils.js'; import { ortbConverter } from '../libraries/ortbConverter/converter.js' +import { ortb25Translator } from '../libraries/ortb2.5Translator/translator.js'; const BIDDER_CODE = 'startio'; const METHOD = 'POST'; @@ -12,9 +13,9 @@ const converter = ortbConverter({ imp(buildImp, bidRequest, context) { const imp = buildImp(bidRequest, context); - if (imp['banner']) { - deepSetValue(imp, 'banner.w', deepAccess(imp, 'banner.format.0.w')); - deepSetValue(imp, 'banner.h', deepAccess(imp, 'banner.format.0.h')); + if (imp?.banner?.format?.[0]) { + imp.banner.w ??= deepAccess(imp, 'banner.format.0.w'); + imp.banner.h ??= deepAccess(imp, 'banner.format.0.h'); } return imp; @@ -22,11 +23,12 @@ const converter = ortbConverter({ request(buildRequest, imps, bidderRequest, context) { const request = buildRequest(imps, bidderRequest, context); const publisherId = deepAccess(bidderRequest, 'bids.0.params.publisherId'); - - if (request['site']) { - deepSetValue(request, 'site.publisher.id', publisherId); - } else { - deepSetValue(request, 'app.publisher.id', publisherId); + if (request?.site) { + request.site.publisher = request.site.publisher || {}; + request.site.publisher.id = publisherId; + } else if (request?.app) { + request.app.publisher = request.app.publisher || {}; + request.app.publisher.id = publisherId; } deepSetValue(request, 'ext.prebid', {}); @@ -49,7 +51,8 @@ const converter = ortbConverter({ context: { netRevenue: true, ttl: 30 - } + }, + translator: ortb25Translator() }); export const spec = { @@ -67,7 +70,7 @@ export const spec = { method: METHOD, url: ENDPOINT_URL, options: { - contentType: 'application/json', + contentType: 'text/plain', withCredentials: false, crossOrigin: true }, @@ -90,7 +93,12 @@ export const spec = { onBidWon: (bid) => { if (bid.nurl) { - triggerNurlWithCpm(bid, bid.cpm) + const url = new URL(bid.nurl); + url.searchParams.set('cpm', bid.cpm); + + fetch(url.toString(), { method: 'GET', keepalive: true }).catch(err => + logError('Error triggering win notification', err) + ); } }, diff --git a/test/spec/modules/startioBidAdapter_spec.js b/test/spec/modules/startioBidAdapter_spec.js index 0bde0003008..6f1d55e458b 100644 --- a/test/spec/modules/startioBidAdapter_spec.js +++ b/test/spec/modules/startioBidAdapter_spec.js @@ -248,6 +248,18 @@ describe('Prebid Adapter: Startio', function () { }) }); + it('should set meta.adomain from the bid response adomain field', () => { + const requests = spec.buildRequests(VALID_MEDIA_TYPES_REQUESTS[BANNER], VALID_BIDDER_REQUEST); + const { data } = requests[0]; + const bids = spec.interpretResponse({ body: SERVER_RESPONSE_BANNER }, { data }).bids; + + expect(bids).to.have.lengthOf(1); + const bid = bids[0]; + + expect(bid.meta).to.be.an('object'); + expect(bid.meta.adomain).to.be.an('array').that.includes('start.io'); + }); + if (FEATURES.VIDEO) { it('should return a valid bid array with a video bid', () => { const requests = spec.buildRequests(VALID_MEDIA_TYPES_REQUESTS[VIDEO], VALID_BIDDER_REQUEST); @@ -276,6 +288,4 @@ describe('Prebid Adapter: Startio', function () { }); } }); - - }); From d8a15c820547ed3265983e473255c16550158f39 Mon Sep 17 00:00:00 2001 From: "Prebid Start.io" Date: Mon, 26 May 2025 13:21:23 +0300 Subject: [PATCH 3/3] Get rid of deep* methods and adjust tests --- modules/startioBidAdapter.js | 14 +++++++------- test/spec/modules/startioBidAdapter_spec.js | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/modules/startioBidAdapter.js b/modules/startioBidAdapter.js index 910e8d0336d..ac9227454a7 100644 --- a/modules/startioBidAdapter.js +++ b/modules/startioBidAdapter.js @@ -1,6 +1,6 @@ import { registerBidder } from '../src/adapters/bidderFactory.js'; import { BANNER, VIDEO, NATIVE } from '../src/mediaTypes.js'; -import { deepAccess, deepSetValue, logError } from '../src/utils.js'; +import { logError } from '../src/utils.js'; import { ortbConverter } from '../libraries/ortbConverter/converter.js' import { ortb25Translator } from '../libraries/ortb2.5Translator/translator.js'; @@ -14,15 +14,15 @@ const converter = ortbConverter({ const imp = buildImp(bidRequest, context); if (imp?.banner?.format?.[0]) { - imp.banner.w ??= deepAccess(imp, 'banner.format.0.w'); - imp.banner.h ??= deepAccess(imp, 'banner.format.0.h'); + imp.banner.w ??= imp.banner.format[0]?.w; + imp.banner.h ??= imp.banner.format[0]?.h; } return imp; }, request(buildRequest, imps, bidderRequest, context) { const request = buildRequest(imps, bidderRequest, context); - const publisherId = deepAccess(bidderRequest, 'bids.0.params.publisherId'); + const publisherId = bidderRequest?.bids?.[0]?.params?.publisherId; if (request?.site) { request.site.publisher = request.site.publisher || {}; request.site.publisher.id = publisherId; @@ -30,12 +30,13 @@ const converter = ortbConverter({ request.app.publisher = request.app.publisher || {}; request.app.publisher.id = publisherId; } - deepSetValue(request, 'ext.prebid', {}); + request.ext = request.ext || {}; + request.ext.prebid = request.ext.prebid || {}; return request; }, bidResponse(buildBidResponse, bid, context) { - const isValidBidType = deepAccess(bid, 'ext.prebid.type') === deepAccess(context, 'mediaType'); + const isValidBidType = bid?.ext?.prebid?.type === context?.mediaType; if (context.mediaType === NATIVE) { const ortb = JSON.parse(bid.adm); @@ -95,7 +96,6 @@ export const spec = { if (bid.nurl) { const url = new URL(bid.nurl); url.searchParams.set('cpm', bid.cpm); - fetch(url.toString(), { method: 'GET', keepalive: true }).catch(err => logError('Error triggering win notification', err) ); diff --git a/test/spec/modules/startioBidAdapter_spec.js b/test/spec/modules/startioBidAdapter_spec.js index 6f1d55e458b..d43d27b861a 100644 --- a/test/spec/modules/startioBidAdapter_spec.js +++ b/test/spec/modules/startioBidAdapter_spec.js @@ -257,7 +257,7 @@ describe('Prebid Adapter: Startio', function () { const bid = bids[0]; expect(bid.meta).to.be.an('object'); - expect(bid.meta.adomain).to.be.an('array').that.includes('start.io'); + expect(bid.meta.advertiserDomains).to.be.an('array').that.includes('start.io'); }); if (FEATURES.VIDEO) {