From 8537247de34c40fcd1368d41d0434e1bd2c17d0b Mon Sep 17 00:00:00 2001 From: Yash Chotaliya Date: Thu, 26 Feb 2026 16:27:52 +0530 Subject: [PATCH 1/2] New adapter - Playstream --- modules/playstreamBidAdapter.js | 476 +++++++++++++ modules/playstreamBidAdapter.md | 69 ++ .../spec/modules/playstreamBidAdapter_spec.js | 674 ++++++++++++++++++ 3 files changed, 1219 insertions(+) create mode 100644 modules/playstreamBidAdapter.js create mode 100644 modules/playstreamBidAdapter.md create mode 100644 test/spec/modules/playstreamBidAdapter_spec.js diff --git a/modules/playstreamBidAdapter.js b/modules/playstreamBidAdapter.js new file mode 100644 index 00000000000..c55dae67c1a --- /dev/null +++ b/modules/playstreamBidAdapter.js @@ -0,0 +1,476 @@ +import { ortbConverter } from '../libraries/ortbConverter/converter.js' +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { BANNER, VIDEO } from '../src/mediaTypes.js'; +import { ajax } from '../src/ajax.js'; +import { + groupBy, + logMessage, + deepAccess, + mergeDeep, + isFn, + isStr, + isPlainObject, +} from '../src/utils.js'; + +const BIDDER_CODE = 'playstream'; +const TTL = 300; +const DEFAULT_CURRENCY = 'USD'; +const NET_REVENUE = true; +const ENDPOINT_PATH = '/server/adserver/hb'; + +const converter = ortbConverter({ + context: { + netRevenue: NET_REVENUE, + ttl: TTL, + currency: DEFAULT_CURRENCY + }, + + imp(buildImp, bidRequest, context) { + const imp = buildImp(bidRequest, context) || {}; + const mediaType = getMediaType(bidRequest); + const sizes = uniqSizes(resolveSizes(bidRequest, mediaType)); + const [w, h] = sizes[0] || [0, 0]; + + imp.id = imp.id || bidRequest.bidId; + + imp.tagid = String(`${bidRequest?.params?.adUnitId}-${bidRequest?.params?.publisherId}`); + + const floor = getFloor(bidRequest, { width: w, height: h }, mediaType); + + if (Number.isFinite(floor) && floor > 0) { + imp.bidfloor = floor; + imp.bidfloorcur = DEFAULT_CURRENCY; + } + + const tid = + deepAccess(bidRequest, 'ortb2Imp.ext.tid') || + deepAccess(bidRequest, 'ortb2Imp.ext.prebid.tid') || + deepAccess(bidRequest, 'ortb2Imp.ext.data.tid'); + + if (tid && !imp.ext.tid) imp.ext.tid = tid; + + mergeDeep(imp, { + ext: { + playstream: { + publisherId: bidRequest?.params?.publisherId, + adUnitId: bidRequest?.params?.adUnitId, + type: mediaType, + sizes: sizes.map(([sw, sh]) => ({ w: sw, h: sh })), + + maxSlotPerPod: toFiniteNumber(bidRequest?.params?.maxSlotPerPod), + maxAdDuration: toFiniteNumber(bidRequest?.params?.maxAdDuration), + } + } + }); + + if (mediaType === BANNER) { + imp.banner = isPlainObject(imp.banner) ? imp.banner : {}; + mergeDeep(imp.banner, { + w: Number(w) || undefined, + h: Number(h) || undefined, + format: sizes.map(([fw, fh]) => ({ w: fw, h: fh })), + topframe: context?.pageCtx?.topframe ? 1 : 0, + }); + if (imp.video) delete imp.video; + } + + if (mediaType === VIDEO) { + imp.video = isPlainObject(imp.video) ? imp.video : {}; + mergeDeep(imp.video, { + w: Number(w) || undefined, + h: Number(h) || undefined, + mimes: deepAccess(bidRequest, 'mediaTypes.video.mimes') || ['video/mp4'], + }); + + const maxAdDur = toFiniteNumber(bidRequest?.params?.maxAdDuration); + if (Number.isFinite(maxAdDur)) { + imp.video.maxduration = maxAdDur; // OpenRTB standard field + } + + if (imp.banner) delete imp.banner; + } + + return imp; + }, + + request(buildRequest, imps, bidderRequest, context) { + const request = buildRequest(imps, bidderRequest, context) || {}; + const pageCtx = context?.pageCtx || {}; + + const pageUrl = + deepAccess(bidderRequest, 'ortb2.site.page') || + deepAccess(bidderRequest, 'refererInfo.page') || + window?.location?.href; + + const ua = + deepAccess(bidderRequest, 'ortb2.device.ua') || + navigator?.userAgent; + + const pubId = getCommonParam(context?.bidRequests, 'publisherId'); + + const schain = getFirstSchain(context?.bidRequests); + + const gdpr = resolveGdpr(bidderRequest, context?.bidRequests); + const consent = resolveConsent(bidderRequest, context?.bidRequests); + + const ip = getFirstParam(context?.bidRequests, 'ip'); + const lat = toFiniteNumber(getFirstParam(context?.bidRequests, 'latitude')); + const lon = toFiniteNumber(getFirstParam(context?.bidRequests, 'longitude')); + + mergeDeep(request, { + site: { + page: pageUrl, + ...(pubId != null ? { publisher: { id: String(pubId) } } : {}) + }, + device: { + ua, + w: Number(pageCtx?.width) || undefined, + h: Number(pageCtx?.height) || undefined, + ...(isNonEmptyString(ip) ? { ip } : {}), + ...(Number.isFinite(lat) || Number.isFinite(lon) + ? { geo: { ...(Number.isFinite(lat) ? { lat } : {}), ...(Number.isFinite(lon) ? { lon } : {}) } } + : {}) + }, + regs: { ext: { gdpr } }, + user: { ext: { ...(consent ? { consent } : {}) } }, + source: { ext: { ...(schain ? { schain } : {}) } }, + ext: { + format: 'web', + referer: bidderRequest?.refererInfo || undefined, + [BIDDER_CODE]: { + pbjs: 1, + pbv: '$prebid.version$' + } + } + }); + + return request; + }, + + bidResponse(buildBidResponse, bid, context) { + const prebidBid = buildBidResponse(bid, context) || {}; + + mergeDeep(prebidBid, { + meta: { + advertiserDomains: Array.isArray(bid?.adomain) ? bid.adomain : [] + } + }); + + const reqMediaType = getMediaTypeFromImp(context?.imp); + let mt = prebidBid.mediaType || reqMediaType || inferMediaTypeFromAdm(bid?.adm); + + if (!mt) { + mt = reqMediaType + } + + prebidBid.mediaType = mt; + prebidBid.meta = isPlainObject(prebidBid.meta) ? prebidBid.meta : {}; + prebidBid.meta.mediaType = mt; + + if (isNonEmptyString(bid?.nurl)) { + prebidBid.nurl = bid.nurl; + } + + if (!prebidBid.width && Number.isFinite(Number(bid?.w))) prebidBid.width = Number(bid.w); + if (!prebidBid.height && Number.isFinite(Number(bid?.h))) prebidBid.height = Number(bid.h); + + if (mt === VIDEO) { + if (isNonEmptyString(bid?.adm) && looksLikeVast(bid.adm)) { + prebidBid.vastXml = bid.adm; + + if (prebidBid.vastUrl) delete prebidBid.vastUrl; + } else if (isNonEmptyString(bid?.nurl)) { + prebidBid.vastUrl = bid.nurl; + } + + if (prebidBid.ad) delete prebidBid.ad; + return prebidBid; + } + + if (mt === BANNER) { + if (isNonEmptyString(bid?.adm)) { + prebidBid.ad = bid.adm; + } + + if (prebidBid.vastXml) delete prebidBid.vastXml; + if (prebidBid.vastUrl) delete prebidBid.vastUrl; + + return prebidBid; + } + + return prebidBid; + } +}); + +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: [VIDEO, BANNER], + + isBidRequestValid: (bid) => { + if (!bid || typeof bid !== 'object') return false; + const p = bid.params || {}; + if (!isNonEmptyString(p.host)) return false; + + const host = sanitizeHost(p.host); + if (!host) return false; + + if (!isNonEmptyString(p.type)) return false; + if (p.type !== VIDEO && p.type !== BANNER) return false; + + if (!isNonEmptyString(p.adUnitId) && typeof p.adUnitId !== 'number') return false; + if (!isNonEmptyString(p.publisherId) && typeof p.publisherId !== 'number') return false; + + if (p.price !== undefined && p.price !== null && p.price !== '') { + const priceNum = Number(p.price); + if (!Number.isFinite(priceNum) || priceNum < 0) return false; + } + + const sizes = resolveSizes(bid); + if (!sizes.length) return false; + + return true; + }, + + buildRequests: (validBidRequests, bidderRequest) => { + if (!Array.isArray(validBidRequests) || !validBidRequests.length) return []; + + const pageCtx = getPageContext(); + + const grouped = groupBy( + validBidRequests.map(br => ({ host: sanitizeHost(br?.params?.host), br })), + 'host' + ); + + const scheme = (location.protocol === 'https:') ? 'https' : 'http'; + + const data = Object.keys(grouped) + .filter(h => isNonEmptyString(h)) + .map((host) => { + const bids = grouped[host].map(x => x.br); + + const { adUnitId, publisherId } = bids[0].params || {}; + + const ortb = converter.toORTB({ + bidderRequest, + bidRequests: bids, + context: { pageCtx } + }); + + return { + method: 'POST', + url: `${scheme}://${host}${ENDPOINT_PATH}?adUnitId=${adUnitId}&publisherId=${publisherId}`, + bids, + data: ortb, + options: { contentType: 'application/json' } + }; + }); + + return data; + }, + + interpretResponse: (serverResponse, request) => { + const body = serverResponse?.body; + + if (!body || !request?.data) return []; + + const converted = converter.fromORTB({ response: body, request: request.data }); + + const bids = Array.isArray(converted?.bids) ? converted.bids : []; + + return bids.filter(b => { + if (b?.meta?.mediaType === VIDEO) return !!b.vastXml || !!b.vastUrl; + if (b?.meta?.mediaType === BANNER) return !!b.ad; + return false; + }); + }, + + onBidWon: (bid) => { + const cpm = Number(bid?.cpm); + if (!Number.isFinite(cpm)) return; + + if (isNonEmptyString(bid?.nurl)) { + const url = bid.nurl.replace(/\$\{AUCTION_PRICE\}/g, String(cpm)); + ajax(url, () => { }, null, { method: 'GET' }); + } + }, + + getUserSyncs: () => [] +} + +registerBidder(spec); + +function getMediaType(bidRequest) { + const t = bidRequest?.params?.type; + return (t === VIDEO || t === BANNER) ? t : BANNER; +} + +function getMediaTypeFromImp(imp) { + if (!imp || typeof imp !== 'object') return null; + if (imp.video) return VIDEO; + if (imp.banner) return BANNER; + return null; +} + +function resolveSizes(bidRequest, mediaType) { + const mt = mediaType || getMediaType(bidRequest); + + if (bidRequest?.mediaTypes) { + if (mt === VIDEO && bidRequest.mediaTypes.video?.playerSize) { + return toSizeArray(bidRequest.mediaTypes.video.playerSize); + } + if (mt === BANNER && bidRequest.mediaTypes.banner?.sizes) { + return toSizeArray(bidRequest.mediaTypes.banner.sizes); + } + } + + return toSizeArray(bidRequest?.sizes); +} + +function toSizeArray(input) { + if (!input) return []; + + if (Array.isArray(input) && input.length === 2 && typeof input[0] === 'number' && typeof input[1] === 'number') { + return [input]; + } + + if (Array.isArray(input) && Array.isArray(input[0])) { + return input; + } + + return []; +} + +function uniqSizes(sizes) { + const seen = new Set(); + const out = []; + + for (const s of (sizes || [])) { + if (!Array.isArray(s) || s.length < 2) continue; + const w = Number(s[0]); + const h = Number(s[1]); + if (!Number.isFinite(w) || !Number.isFinite(h) || w <= 0 || h <= 0) continue; + + const key = `${w}x${h}`; + if (seen.has(key)) continue; + + seen.add(key); + out.push([w, h]); + } + + return out; +} + +function sanitizeHost(host) { + if (!isNonEmptyString(host)) return null; + + let h = host.trim(); + h = h.replace(/^https?:\/\//i, ''); + h = h.split('/')[0].split('?')[0].split('#')[0]; + + if (!h || /\s/.test(h)) return null; + return h; +} + +function isNonEmptyString(v) { + return typeof v === 'string' && v.trim().length > 0; +} + +function toFiniteNumber(v) { + const n = Number(v); + return Number.isFinite(n) ? n : undefined; +} + +function getPageContext() { + const ctx = { width: 0, height: 0, topframe: false }; + + try { + ctx.width = window.top.screen.width; + ctx.height = window.top.screen.height; + window.top.location.toString(); + ctx.topframe = true; + } catch (error) { + logMessage('Error getting top frame context', error); + ctx.width = window.screen.width; + ctx.height = window.screen.height; + ctx.topframe = false; + } + + return ctx; +} + +function getFloor(bidRequest, size, mediaType) { + try { + const paramFloor = Number(bidRequest?.params?.price); + if (Number.isFinite(paramFloor) && paramFloor > 0) { + return paramFloor; + } + } catch (error) { } + + if (!isFn(bidRequest?.getFloor)) return; + + try { + const bidFloor = bidRequest.getFloor({ + currency: DEFAULT_CURRENCY, + mediaType, + size: [size.width, size.height], + }); + + if (isPlainObject(bidFloor) && !isNaN(bidFloor.floor) && bidFloor.currency === DEFAULT_CURRENCY) { + return bidFloor.floor; + } + } catch { } +} + +function getFirstSchain(bidRequests) { + for (const br of (bidRequests || [])) { + const schain = + deepAccess(br, 'ortb2.source.ext.schain') || + deepAccess(br, 'ortb2Imp.ext.schain') || + deepAccess(br, 'ortb2Imp.ext.prebid.schain'); + if (schain) return schain; + } + return null; +} + +function getCommonParam(bidRequests, key) { + const vals = new Set(); + for (const br of (bidRequests || [])) { + const v = br?.params?.[key]; + if (v != null) vals.add(String(v)); + } + return vals.size === 1 ? Array.from(vals)[0] : null; +} + +function getFirstParam(bidRequests, key) { + for (const br of (bidRequests || [])) { + const v = br?.params?.[key]; + if (v != null && v !== '') return v; + } + return undefined; +} + +function resolveGdpr(bidderRequest, bidRequests) { + const applies = bidderRequest?.gdprConsent?.gdprApplies; + if (typeof applies === 'boolean') return applies ? 1 : 0; + + const v = getFirstParam(bidRequests, 'gdpr'); + if (v == null) return 0; + + return Number(v) ? 1 : 0; +} + +function resolveConsent(bidderRequest, bidRequests) { + const cs = bidderRequest?.gdprConsent?.consentString; + if (isNonEmptyString(cs)) return cs; + + const v = getFirstParam(bidRequests, 'consent'); + return isNonEmptyString(v) ? v : undefined; +} + +function looksLikeVast(adm) { + return isStr(adm) && /<\s*VAST[\s>]/i.test(adm); +} + +function inferMediaTypeFromAdm(adm) { + return looksLikeVast(adm) ? VIDEO : BANNER; +} diff --git a/modules/playstreamBidAdapter.md b/modules/playstreamBidAdapter.md new file mode 100644 index 00000000000..9fc55225e78 --- /dev/null +++ b/modules/playstreamBidAdapter.md @@ -0,0 +1,69 @@ +# Overview + +``` +Module Name: Playstream Bidder Adapter +Module Type: Bidder Adapter +Maintainer: kush@adsolut.in +``` + +# Description + +Module that connects to Playstream demand sources + +# Test Parameters for banner +``` +var adUnits = [{ + code: 'pokluijh-polk-polk-pokl-pokluijhytfg', + mediaTypes: { + banner: { + sizes: [[300, 250], [728, 90]] + } + }, + bids: [{ + bidder: 'playstream', + params: { + host: 'exchange-9qao.ortb.net', + adUnitId: '697871ac0ec1c6100e1f9121', + publisherId: '697871ac0ec1c6100e1f9122', + type: 'banner', + ip: '127.0.0.1', + latitude: 23.21, + longitude: -23.21, + maxSlotPerPod: 3, + maxAdDuration: 120, + gdpr: 0, + consent: '' + } + }] +}]; +``` + +# Test Parameters for video +``` +var videoAdUnit = [{ + code: 'poiuytre-lkjh-gfds-mnbv-lkjhfgsdxcbv', + mediaTypes: { + video: { + playerSize: [640, 360], + context: 'instream' + } + }, + sizes: [640, 360], + bids: [{ + bidder: 'playstream', + params: { + host: 'exchange-9qao.ortb.net', + adUnitId: '697871ac0ec1c6100e1f9121', + publisherId: '697871ac0ec1c6100e1f9122', + type: 'video', + ip: '127.0.0.1', + latitude: 23.21, + longitude: -23.21, + maxSlotPerPod: 3, + maxAdDuration: 120, + gdpr: 0, + consent: '' + } + }] +}]; +``` diff --git a/test/spec/modules/playstreamBidAdapter_spec.js b/test/spec/modules/playstreamBidAdapter_spec.js new file mode 100644 index 00000000000..13387939075 --- /dev/null +++ b/test/spec/modules/playstreamBidAdapter_spec.js @@ -0,0 +1,674 @@ +import { expect } from 'chai'; +import { spec } from '../../../modules/playstreamBidAdapter.js'; +import { BANNER, VIDEO } from '../../../src/mediaTypes.js'; + +describe('playstreamBidAdapter', function () { + const BID_PERFECT = { + bidId: '3ee692b3c7392e', + bidder: 'playstream', + bidderRequestId: '256f2e7b8948da', + params: { + host: 'exchange.ortb.net', + adUnitId: '697871ac0ec1c6100e1f9121', + publisherId: '697871ac0ec1c6100e1f9122', + type: 'banner', + ip: '127.0.0.1', + latitude: 23.21, + longitude: -23.21, + maxSlotPerPod: 3, + maxAdDuration: 120, + gdpr: 0, + consent: '' + }, + placementCode: 'placement_123', + auctionId: '85a8971a-ba3e-5d02-97a0-2c355cc0c6e3', + mediaTypes: { + banner: { + sizes: [[300, 250]] + } + }, + ortb2Imp: { + ext: { + gpid: '96b9a82b-cb4f-6e13-a8b1-3d466dd1d7f4', + tid: '849e6a26-7762-54ca-ac7c-e61628461a28', + data: { + pbadslot: '96b9a82b-cb4f-6e13-a8b1-3d466dd1d7f4' + } + } + }, + userIdAsEids: [ + { + source: 'test.com', + uids: [ + { + id: '95af7b37-8873-65db-bd8d-f72739572b39', + } + ] + } + ], + ortb2: { + source: { + ext: { + schain: { + ver: '1.0', + complete: 1, + nodes: [ + { + asi: 'example.com', + sid: '1', + hp: 1 + } + ] + } + } + } + } + }; + const BID_WORKS = { + bidId: '69ff0981d4275b', + bidder: 'playstream', + bidderRequestId: '31a0eb02d9275a', + params: { + host: 'ads.playstream.media', + adUnitId: '697871ac0ec1c6100e1f9123', + publisherId: '697871ac0ec1c6100e1f9124', + type: 'banner', + }, + placementCode: 'placement_456', + auctionId: '593g99ef-3abc-56d9-a92b-e36f4a565b45', + sizes: [[350, 200]], + ortb2Imp: { + ext: { + gpid: '6a4h00fg-4bcd-67ea-b03c-f47g5b676c56', + tid: '849e6a26-7762-54ca-ac7c-e61628461a28', + data: { + 'pbadslot': '6a4h00fg-4bcd-67ea-b03c-f47g5b676c56' + } + } + }, + userIdAsEids: [ + { + source: 'test.org', + uids: [ + { + id: '95af7b37-8873-65db-bd8d-f72739572b39', + } + ] + } + ], + ortb2: { + source: { + ext: { + schain: { + ver: '1.0', + complete: 1, + nodes: [ + { + asi: 'example.co.in', + sid: '1', + hp: 1 + }, + { + asi: 'example.in', + sid: '2', + hp: 1 + } + ] + } + } + } + } + }; + const BID_SAME_HOST = { + bidId: '3ee692b3c7392e', + bidder: 'playstream', + bidderRequestId: '256f2e7b8948da', + params: { + host: 'exchange.ortb.net', + adUnitId: '697871ac0ec1c6100e1f9121', + publisherId: '697871ac0ec1c6100e1f9122', + type: 'banner', + ip: '127.0.0.1', + latitude: 23.21, + longitude: -23.21, + maxSlotPerPod: 3, + maxAdDuration: 120, + gdpr: 0, + consent: '' + }, + placementCode: 'placement_123', + auctionId: '85a8971a-ba3e-5d02-97a0-2c355cc0c6e3', + mediaTypes: { + banner: { + sizes: [[300, 250]] + } + }, + ortb2Imp: { + ext: { + gpid: '96b9a82b-cb4f-6e13-a8b1-3d466dd1d7f4', + tid: '849e6a26-7762-54ca-ac7c-e61628461a28', + data: { + 'pbadslot': '96b9a82b-cb4f-6e13-a8b1-3d466dd1d7f4' + } + } + }, + userIdAsEids: [ + { + source: 'test.com', + uids: [ + { + id: '95af7b37-8873-65db-bd8d-f72739572b39', + } + ] + } + ], + ortb2: { + source: { + ext: { + schain: { + ver: '1.0', + complete: 1, + nodes: [ + { + asi: 'example.com', + sid: '1', + hp: 1 + } + ] + } + } + } + } + }; + const BID_VIDEO_VALID = { + bidId: 'b98b728e1a3f61', + bidder: 'playstream', + bidderRequestId: '256f2e7b8948da', + params: { + host: 'exchange.ortb.net', + adUnitId: '697871ac0ec1c6100e1f9121', + publisherId: '697871ac0ec1c6100e1f9122', + type: 'video', + ip: '127.0.0.1', + latitude: 23.21, + longitude: -23.21, + maxSlotPerPod: 3, + maxAdDuration: 120, + gdpr: 0, + consent: '' + }, + placementCode: 'placement_123', + auctionId: '85a8971a-ba3e-5d02-97a0-2c355cc0c6e3', + mediaTypes: { + video: { + playerSize: [[640, 360]], + context: 'instream' + } + }, + ortb2Imp: { + ext: { + gpid: '96b9a82b-cb4f-6e13-a8b1-3d466dd1d7f4', + tid: '849e6a26-7762-54ca-ac7c-e61628461a28', + data: { + pbadslot: '96b9a82b-cb4f-6e13-a8b1-3d466dd1d7f4' + } + } + }, + userIdAsEids: [ + { + source: 'test.com', + uids: [ + { + id: '95af7b37-8873-65db-bd8d-f72739572b39', + } + ] + } + ], + ortb2: { + source: { + ext: { + schain: { + ver: '1.0', + complete: 1, + nodes: [ + { + asi: 'example.com', + sid: '1', + hp: 1 + } + ] + } + } + } + } + }; + const BID_WITHOUT_HOST = { + bidId: '120756d8e70571', + bidder: 'playstream', + bidderRequestId: 'g3c26g90f88cb7', + params: { + adUnitId: '697871ac0ec1c6100e1f9125', + publisherId: '697871ac0ec1c6100e1f9126', + type: 'video' + }, + placementCode: 'placement_789', + auctionId: 'f5882254-7bb8-52fd-9935-dfe5453d07d9', + sizes: [[800, 600]], + ortb2Imp: { + ext: { + gpid: 'g6993365-8cc9-63ge-0046-efg6564e18e0', + tid: '849e6a26-7762-54ca-0c7c-e61628461028', + data: { + 'pbadslot': 'g6993365-8cc9-63ge-0046-efg6564e18e0' + } + } + }, + userIdAsEids: [ + { + source: 'test.co.in', + uids: [ + { + id: '950f7b37-8873-65db-1d8d-f72739572139', + }, + { + id: '061g8c48-9984-76ec-2e9e-g83840683240', + } + ] + } + ], + ortb2: { + source: { + ext: { + schain: { + ver: '1.0', + complete: 1, + nodes: [ + { + asi: 'example.ac.in', + sid: '1', + hp: 1 + } + ] + } + } + } + } + }; + const BID_WITHOUT_REQUIRED_FIELDS = { + bidId: '120756d8e70571', + bidder: 'playstream', + bidderRequestId: 'g3c26g90f88cb7', + params: { + host: 'exchange.ortb.net', + type: 'video', + }, + placementCode: 'placement_789', + auctionId: 'f5882254-7bb8-52fd-9935-dfe5453d07d9', + sizes: [[800, 600]], + ortb2Imp: { + ext: { + gpid: 'g6993365-8cc9-63ge-0046-efg6564e18e0', + tid: '849e6a26-7762-54ca-0c7c-e61628461028', + data: { + 'pbadslot': 'g6993365-8cc9-63ge-0046-efg6564e18e0' + } + } + }, + userIdAsEids: [ + { + source: 'test.co.in', + uids: [ + { + id: '950f7b37-8873-65db-1d8d-f72739572139', + }, + { + id: '061g8c48-9984-76ec-2e9e-g83840683240', + } + ] + } + ], + ortb2: { + source: { + ext: { + schain: { + ver: '1.0', + complete: 1, + nodes: [ + { + asi: 'example.ac.in', + sid: '1', + hp: 1 + } + ] + } + } + } + } + }; + + describe('buildRequests', function () { + const bidderRequest = { + ortb2: { + device: { + sua: { + browsers: [], + platform: [], + mobile: 1, + architecture: 'arm' + } + } + }, + refererInfo: { + page: 'testPage' + } + } + const serverRequests = spec.buildRequests([BID_PERFECT, BID_WORKS, BID_SAME_HOST], bidderRequest) + + it('Creates two ServerRequests', function () { + expect(serverRequests).to.exist + expect(serverRequests).to.have.lengthOf(2) + }) + + serverRequests.forEach(serverRequest => { + it('Creates a ServerRequest object with method, URL and OpenRTB data', function () { + expect(serverRequest).to.exist + expect(serverRequest.method).to.equal('POST'); + expect(serverRequest.url).to.be.a('string'); + expect(serverRequest.data).to.be.an('object'); + expect(serverRequest.options).to.be.an('object'); + }) + + it('OpenRTB request has core top-level fields', function () { + const ortb = serverRequest.data; + + expect(ortb).to.have.property('imp'); + expect(ortb.imp).to.be.an('array').that.is.not.empty; + + expect(ortb).to.have.property('site'); + expect(ortb).to.have.property('device'); + expect(ortb).to.have.property('ext'); + + expect(ortb.ext).to.be.an('object'); + expect(ortb.ext).to.have.property('format'); + + expect(ortb.device).to.be.an('object'); + expect(ortb.device).to.have.property('sua'); + expect(ortb.device.sua).to.deep.equal(bidderRequest.ortb2.device.sua); + }); + }) + + it('Returns valid URLs', function () { + const urls = serverRequests.map(r => r.url); + expect(urls).to.have.members([ + 'http://exchange.ortb.net/server/adserver/hb?adUnitId=697871ac0ec1c6100e1f9121&publisherId=697871ac0ec1c6100e1f9122', + 'http://ads.playstream.media/server/adserver/hb?adUnitId=697871ac0ec1c6100e1f9123&publisherId=697871ac0ec1c6100e1f9124' + ]); + }); + + it('Groups same-host bids into a single OpenRTB request with multiple imps', function () { + const reqExchange = serverRequests.find(r => r.url.includes('exchange.ortb.net')); + const reqAds = serverRequests.find(r => r.url.includes('ads.playstream.media')); + + expect(reqExchange).to.exist; + expect(reqAds).to.exist; + + expect(reqExchange.data.imp).to.have.lengthOf(2); + expect(reqAds.data.imp).to.have.lengthOf(1); + }); + + it('Each bidRequest becomes one imp and carries ortb2Imp + your custom ext.playstream fields', function () { + const reqExchange = serverRequests.find(r => r.url.includes('exchange.ortb.net')); + const reqAds = serverRequests.find(r => r.url.includes('ads.playstream.media')); + + validateImp(reqExchange.data.imp[0], BID_PERFECT); + validateImp(reqExchange.data.imp[1], BID_SAME_HOST); + + validateImp(reqAds.data.imp[0], BID_WORKS); + }); + + it('site.page prefers bidderRequest.ortb2.site.page when provided (otherwise refererInfo.page)', function () { + const br = { + ortb2: { + site: { page: 'testSitePage' }, + device: { sua: { browsers: [], platform: [], mobile: 1, architecture: 'arm' } } + }, + refererInfo: { page: 'ignoredPage' } + }; + + const srs = spec.buildRequests([BID_PERFECT], br); + expect(srs).to.have.lengthOf(1); + expect(srs[0].data.site.page).to.equal('testSitePage'); + }); + + it('Returns empty array if no valid requests are passed', function () { + const srs = spec.buildRequests([]); + expect(srs).to.be.an('array').that.is.empty; + }); + }); + + describe('interpretResponse', function () { + it('Banner: returns valid Prebid bids from an OpenRTB response', function () { + const bidderRequest = { refererInfo: { page: 'testPage' } }; + const [sr] = spec.buildRequests([BID_PERFECT], bidderRequest); + + const ortbBannerResponse = { + body: { + id: 'resp-1', + cur: 'USD', + seatbid: [ + { + seat: 'ps', + bid: [ + { + impid: BID_PERFECT.bidId, + price: 15, + adm: '

Playstream Ad

', + crid: 'fa23f84bba855b', + adomain: ['example.com'], + w: 300, + h: 250, + mtype: 1, + } + ] + } + ] + } + }; + + const bids = spec.interpretResponse(ortbBannerResponse, sr); + expect(bids).to.be.an('array').that.is.not.empty; + + const b = bids[0]; + expect(b).to.include.keys('requestId', 'cpm', 'currency', 'ttl', 'creativeId', 'netRevenue', 'meta'); + expect(b.requestId).to.equal(BID_PERFECT.bidId); + expect(b.cpm).to.equal(15); + expect(b.currency).to.equal('USD'); + expect(b.meta).to.be.an('object'); + expect(b.meta.advertiserDomains).to.deep.equal(['example.com']); + expect(b.meta.mediaType).to.equal('banner'); + expect(b).to.have.property('ad'); + expect(b.ad).to.be.a('string'); + }); + + it('Video: returns valid Prebid bids from an OpenRTB response (VAST in adm)', function () { + const bidderRequest = { refererInfo: { page: 'testPage' } }; + const [sr] = spec.buildRequests([BID_VIDEO_VALID], bidderRequest); + + const ortbVideoResponse = { + body: { + id: 'resp-2', + cur: 'USD', + seatbid: [ + { + seat: 'ps', + bid: [ + { + impid: BID_VIDEO_VALID.bidId, + price: 15, + adm: '', + crid: 'ce58d726f5d1b9', + adomain: ['example.com'], + w: 640, + h: 360, + mtype: 2 + } + ] + } + ] + } + }; + + const bids = spec.interpretResponse(ortbVideoResponse, sr); + expect(bids).to.be.an('array').that.is.not.empty; + + const b = bids[0]; + expect(b).to.include.keys('requestId', 'cpm', 'currency', 'ttl', 'creativeId', 'netRevenue', 'meta'); + expect(b.requestId).to.equal(BID_VIDEO_VALID.bidId); + expect(b.cpm).to.equal(15); + expect(b.currency).to.equal('USD'); + expect(b.meta.advertiserDomains).to.deep.equal(['example.com']); + expect(b.meta.mediaType).to.equal('video'); + expect(b).to.have.property('vastXml'); + expect(b.vastXml).to.be.a('string'); + }); + + it('Returns empty array if invalid response is passed', function () { + const bids = spec.interpretResponse('invalid_response', null); + expect(bids).to.be.an('array').that.is.empty; + }); + + it('Filters out empty banner bids when adm is missing', function () { + const bidderRequest = { refererInfo: { page: 'testPage' } }; + const [sr] = spec.buildRequests([BID_PERFECT], bidderRequest); + + const resp = { + body: { + id: 'resp-3', + cur: 'USD', + seatbid: [{ bid: [{ impid: BID_PERFECT.bidId, price: 10, crid: 'x', adomain: ['example.com'], w: 300, h: 250 }] }] + } + }; + + const bids = spec.interpretResponse(resp, sr); + expect(bids).to.be.an('array').that.is.empty; + }); + }); + + describe('isBidRequestValid', function () { + it('should return true when required params found', function () { + [BID_PERFECT, BID_WORKS].forEach(bid => { + expect(spec.isBidRequestValid(bid)).to.equal(true); + }); + }); + + it('should return false when required params are not passed', function () { + [BID_WITHOUT_HOST, BID_WITHOUT_REQUIRED_FIELDS].forEach(bid => { + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + }); + }); + + describe('getUserSyncs', function () { + it('should always return empty array (user sync disabled)', function () { + const serverResponses = [{ body: {} }]; + const syncOptions = { iframeEnabled: true, pixelEnabled: true }; + + expect(spec.getUserSyncs(syncOptions, serverResponses)).to.be.an('array').that.is.empty; + }); + }); +}); + +function validateImp(imp, bid) { + expect(imp).to.be.an('object'); + + expect(imp.id).to.equal(bid.bidId); + + expect(imp.tagid).to.equal(`${bid.params.adUnitId}-${bid.params.publisherId}`); + + expect(imp.ext).to.be.an('object'); + expect(imp.ext.tid).to.equal(bid.ortb2Imp?.ext?.tid); + expect(imp.ext.gpid).to.equal(bid.ortb2Imp?.ext?.gpid); + expect(imp.ext.data).to.deep.equal(bid.ortb2Imp?.ext?.data); + + expect(imp.ext.playstream).to.be.an('object'); + expect(imp.ext.playstream.publisherId).to.equal(bid.params.publisherId); + expect(imp.ext.playstream.adUnitId).to.equal(bid.params.adUnitId); + expect(imp.ext.playstream.type).to.equal(bid.params.type); + + const sizes = uniqSizes(resolveSizesForTest(bid)); + expect(imp.ext.playstream.sizes).to.deep.equal( + sizes.map(([w, h]) => ({ w, h })) + ); + + expectOptionalType(imp.ext.playstream, 'maxSlotPerPod', 'number'); + expectOptionalType(imp.ext.playstream, 'maxAdDuration', 'number'); + + if (bid.params.type === 'banner') { + expect(imp).to.have.property('banner'); + expect(imp.banner).to.be.an('object'); + expect(imp.banner).to.have.property('w'); + expect(imp.banner).to.have.property('h'); + } else if (bid.params.type === 'video') { + expect(imp).to.have.property('video'); + expect(imp.video).to.be.an('object'); + expect(imp.video).to.have.property('w'); + expect(imp.video).to.have.property('h'); + } +} + +function expectOptionalType(obj, key, type) { + if (obj[key] !== undefined && obj[key] !== null) { + expect(obj[key]).to.be.a(type); + } +} + +function resolveSizesForTest(bid) { + let sizes = []; + + if (bid.mediaTypes) { + if (bid.params.type === VIDEO && bid.mediaTypes.video?.playerSize) { + sizes = toSizeArray(bid.mediaTypes.video.playerSize); + } else if (bid.params.type === BANNER && bid.mediaTypes.banner?.sizes) { + sizes = toSizeArray(bid.mediaTypes.banner.sizes); + } else { + sizes = toSizeArray(bid.sizes); + } + } + + sizes = sizes.concat(toSizeArray(bid.sizes)); + return sizes; +} + +function toSizeArray(input) { + if (!input) return []; + + if (Array.isArray(input) && input.length === 2 && typeof input[0] === 'number' && typeof input[1] === 'number') { + return [input]; + } + + if (Array.isArray(input) && Array.isArray(input[0])) { + return input; + } + + return []; +} + +function uniqSizes(sizes) { + const seen = new Set(); + const out = []; + + for (const s of (sizes || [])) { + if (!Array.isArray(s) || s.length < 2) continue; + const w = Number(s[0]); + const h = Number(s[1]); + if (!Number.isFinite(w) || !Number.isFinite(h) || w <= 0 || h <= 0) continue; + + const key = `${w}x${h}`; + if (seen.has(key)) continue; + + seen.add(key); + out.push([w, h]); + } + + return out; +} From 5646741fcd3cf9278f47cb14e752e765ac24e348 Mon Sep 17 00:00:00 2001 From: Yash Chotaliya Date: Mon, 16 Mar 2026 19:17:41 +0530 Subject: [PATCH 2/2] Refactor playstreamBidAdapter: streamline request parameters and enforce HTTPS protocol --- modules/playstreamBidAdapter.js | 70 +------------------ modules/playstreamBidAdapter.md | 12 +--- .../spec/modules/playstreamBidAdapter_spec.js | 22 ++---- 3 files changed, 9 insertions(+), 95 deletions(-) diff --git a/modules/playstreamBidAdapter.js b/modules/playstreamBidAdapter.js index c55dae67c1a..4ba3a4bcd74 100644 --- a/modules/playstreamBidAdapter.js +++ b/modules/playstreamBidAdapter.js @@ -17,6 +17,7 @@ const TTL = 300; const DEFAULT_CURRENCY = 'USD'; const NET_REVENUE = true; const ENDPOINT_PATH = '/server/adserver/hb'; +const PROTOCOL = 'https'; const converter = ortbConverter({ context: { @@ -42,13 +43,6 @@ const converter = ortbConverter({ imp.bidfloorcur = DEFAULT_CURRENCY; } - const tid = - deepAccess(bidRequest, 'ortb2Imp.ext.tid') || - deepAccess(bidRequest, 'ortb2Imp.ext.prebid.tid') || - deepAccess(bidRequest, 'ortb2Imp.ext.data.tid'); - - if (tid && !imp.ext.tid) imp.ext.tid = tid; - mergeDeep(imp, { ext: { playstream: { @@ -95,45 +89,18 @@ const converter = ortbConverter({ request(buildRequest, imps, bidderRequest, context) { const request = buildRequest(imps, bidderRequest, context) || {}; - const pageCtx = context?.pageCtx || {}; - - const pageUrl = - deepAccess(bidderRequest, 'ortb2.site.page') || - deepAccess(bidderRequest, 'refererInfo.page') || - window?.location?.href; - - const ua = - deepAccess(bidderRequest, 'ortb2.device.ua') || - navigator?.userAgent; const pubId = getCommonParam(context?.bidRequests, 'publisherId'); - const schain = getFirstSchain(context?.bidRequests); - - const gdpr = resolveGdpr(bidderRequest, context?.bidRequests); - const consent = resolveConsent(bidderRequest, context?.bidRequests); - const ip = getFirstParam(context?.bidRequests, 'ip'); - const lat = toFiniteNumber(getFirstParam(context?.bidRequests, 'latitude')); - const lon = toFiniteNumber(getFirstParam(context?.bidRequests, 'longitude')); mergeDeep(request, { site: { - page: pageUrl, ...(pubId != null ? { publisher: { id: String(pubId) } } : {}) }, device: { - ua, - w: Number(pageCtx?.width) || undefined, - h: Number(pageCtx?.height) || undefined, ...(isNonEmptyString(ip) ? { ip } : {}), - ...(Number.isFinite(lat) || Number.isFinite(lon) - ? { geo: { ...(Number.isFinite(lat) ? { lat } : {}), ...(Number.isFinite(lon) ? { lon } : {}) } } - : {}) }, - regs: { ext: { gdpr } }, - user: { ext: { ...(consent ? { consent } : {}) } }, - source: { ext: { ...(schain ? { schain } : {}) } }, ext: { format: 'web', referer: bidderRequest?.refererInfo || undefined, @@ -241,8 +208,6 @@ export const spec = { 'host' ); - const scheme = (location.protocol === 'https:') ? 'https' : 'http'; - const data = Object.keys(grouped) .filter(h => isNonEmptyString(h)) .map((host) => { @@ -258,10 +223,10 @@ export const spec = { return { method: 'POST', - url: `${scheme}://${host}${ENDPOINT_PATH}?adUnitId=${adUnitId}&publisherId=${publisherId}`, + url: `${PROTOCOL}://${host}${ENDPOINT_PATH}?adUnitId=${adUnitId}&publisherId=${publisherId}`, bids, data: ortb, - options: { contentType: 'application/json' } + options: { contentType: 'text/plain' } }; }); @@ -421,17 +386,6 @@ function getFloor(bidRequest, size, mediaType) { } catch { } } -function getFirstSchain(bidRequests) { - for (const br of (bidRequests || [])) { - const schain = - deepAccess(br, 'ortb2.source.ext.schain') || - deepAccess(br, 'ortb2Imp.ext.schain') || - deepAccess(br, 'ortb2Imp.ext.prebid.schain'); - if (schain) return schain; - } - return null; -} - function getCommonParam(bidRequests, key) { const vals = new Set(); for (const br of (bidRequests || [])) { @@ -449,24 +403,6 @@ function getFirstParam(bidRequests, key) { return undefined; } -function resolveGdpr(bidderRequest, bidRequests) { - const applies = bidderRequest?.gdprConsent?.gdprApplies; - if (typeof applies === 'boolean') return applies ? 1 : 0; - - const v = getFirstParam(bidRequests, 'gdpr'); - if (v == null) return 0; - - return Number(v) ? 1 : 0; -} - -function resolveConsent(bidderRequest, bidRequests) { - const cs = bidderRequest?.gdprConsent?.consentString; - if (isNonEmptyString(cs)) return cs; - - const v = getFirstParam(bidRequests, 'consent'); - return isNonEmptyString(v) ? v : undefined; -} - function looksLikeVast(adm) { return isStr(adm) && /<\s*VAST[\s>]/i.test(adm); } diff --git a/modules/playstreamBidAdapter.md b/modules/playstreamBidAdapter.md index 9fc55225e78..ff4260708c7 100644 --- a/modules/playstreamBidAdapter.md +++ b/modules/playstreamBidAdapter.md @@ -26,13 +26,7 @@ var adUnits = [{ adUnitId: '697871ac0ec1c6100e1f9121', publisherId: '697871ac0ec1c6100e1f9122', type: 'banner', - ip: '127.0.0.1', - latitude: 23.21, - longitude: -23.21, - maxSlotPerPod: 3, - maxAdDuration: 120, - gdpr: 0, - consent: '' + ip: '127.0.0.1' } }] }]; @@ -57,12 +51,8 @@ var videoAdUnit = [{ publisherId: '697871ac0ec1c6100e1f9122', type: 'video', ip: '127.0.0.1', - latitude: 23.21, - longitude: -23.21, maxSlotPerPod: 3, maxAdDuration: 120, - gdpr: 0, - consent: '' } }] }]; diff --git a/test/spec/modules/playstreamBidAdapter_spec.js b/test/spec/modules/playstreamBidAdapter_spec.js index 13387939075..e66fa1ac46e 100644 --- a/test/spec/modules/playstreamBidAdapter_spec.js +++ b/test/spec/modules/playstreamBidAdapter_spec.js @@ -13,12 +13,8 @@ describe('playstreamBidAdapter', function () { publisherId: '697871ac0ec1c6100e1f9122', type: 'banner', ip: '127.0.0.1', - latitude: 23.21, - longitude: -23.21, maxSlotPerPod: 3, - maxAdDuration: 120, - gdpr: 0, - consent: '' + maxAdDuration: 120 }, placementCode: 'placement_123', auctionId: '85a8971a-ba3e-5d02-97a0-2c355cc0c6e3', @@ -129,12 +125,8 @@ describe('playstreamBidAdapter', function () { publisherId: '697871ac0ec1c6100e1f9122', type: 'banner', ip: '127.0.0.1', - latitude: 23.21, - longitude: -23.21, maxSlotPerPod: 3, - maxAdDuration: 120, - gdpr: 0, - consent: '' + maxAdDuration: 120 }, placementCode: 'placement_123', auctionId: '85a8971a-ba3e-5d02-97a0-2c355cc0c6e3', @@ -190,12 +182,8 @@ describe('playstreamBidAdapter', function () { publisherId: '697871ac0ec1c6100e1f9122', type: 'video', ip: '127.0.0.1', - latitude: 23.21, - longitude: -23.21, maxSlotPerPod: 3, - maxAdDuration: 120, - gdpr: 0, - consent: '' + maxAdDuration: 120 }, placementCode: 'placement_123', auctionId: '85a8971a-ba3e-5d02-97a0-2c355cc0c6e3', @@ -400,8 +388,8 @@ describe('playstreamBidAdapter', function () { it('Returns valid URLs', function () { const urls = serverRequests.map(r => r.url); expect(urls).to.have.members([ - 'http://exchange.ortb.net/server/adserver/hb?adUnitId=697871ac0ec1c6100e1f9121&publisherId=697871ac0ec1c6100e1f9122', - 'http://ads.playstream.media/server/adserver/hb?adUnitId=697871ac0ec1c6100e1f9123&publisherId=697871ac0ec1c6100e1f9124' + 'https://exchange.ortb.net/server/adserver/hb?adUnitId=697871ac0ec1c6100e1f9121&publisherId=697871ac0ec1c6100e1f9122', + 'https://ads.playstream.media/server/adserver/hb?adUnitId=697871ac0ec1c6100e1f9123&publisherId=697871ac0ec1c6100e1f9124' ]); });