diff --git a/modules/asterioBidAdapter.md b/modules/asterioBidAdapter.md index f18b7156bd..07a056caf0 100644 --- a/modules/asterioBidAdapter.md +++ b/modules/asterioBidAdapter.md @@ -9,7 +9,7 @@ Maintainer: mnikulin@asteriosoft.com # Description Connects to Asterio Bidder for bids. -Asterio bid adapter supports Banner and Video ads. +Asterio bid adapter supports Banner, Video and Native ads. # Bid Params diff --git a/modules/asterioBidAdapter.ts b/modules/asterioBidAdapter.ts index 313eb4420b..f2501b0f10 100644 --- a/modules/asterioBidAdapter.ts +++ b/modules/asterioBidAdapter.ts @@ -1,7 +1,7 @@ import { type AdapterRequest, type BidderSpec, type ServerResponse, registerBidder } from '../src/adapters/bidderFactory.js'; import { deepAccess, deepClone } from '../src/utils.js'; import { ajax } from '../src/ajax.js'; -import { BANNER, VIDEO } from '../src/mediaTypes.js'; +import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js'; import type { BidRequest } from '../src/adapterManager.js'; import type { Size } from '../src/types/common.d.ts'; @@ -41,9 +41,54 @@ type AsterioServerBid = { adomain?: string[]; }; +type AsterioNativeImage = { + url: string; + width?: number; + height?: number; +}; + +type AsterioNativeResponse = { + link?: { + url?: string; + clicktrackers?: string[]; + }; + imptrackers?: string[]; + eventtrackers?: Array<{ + event?: number; + method?: number; + url?: string; + }>; + assets?: Array<{ + title?: { + text?: string; + }; + img?: { + url?: string; + w?: number; + h?: number; + type?: number; + }; + data?: { + value?: string; + type?: number; + }; + }>; +}; + +type AsterioNativeBid = { + clickUrl?: string; + clickTrackers: string[]; + impressionTrackers: string[]; + ortb: AsterioNativeResponse; + title?: string; + image?: AsterioNativeImage; + icon?: AsterioNativeImage; + body?: string; +}; + export const spec: BidderSpec = { code: BIDDER_CODE, - supportedMediaTypes: [BANNER, VIDEO], + supportedMediaTypes: [BANNER, VIDEO, NATIVE], isBidRequestValid: function (bid) { return !!(bid.params && bid.params.adUnitToken); @@ -54,7 +99,7 @@ export const spec: BidderSpec = { bidId: bidRequest.bidId, adUnitToken: bidRequest.params.adUnitToken, pos: getPosition(bidRequest), - sizes: prepareSizes(bidRequest.sizes) + sizes: prepareSizes(getSizes(bidRequest)) })); const payload: { @@ -117,6 +162,14 @@ export const spec: BidderSpec = { bid.vastXml = bidResponse.ad; } + if (NATIVE === bid.mediaType && bidResponse.ad) { + const native = parseNativeAd(bidResponse.ad); + if (native) { + bid.native = native; + delete bid.ad; + } + } + bid.meta = {}; bid.meta.advertiserDomains = bid.adomain || []; @@ -134,16 +187,68 @@ export const spec: BidderSpec = { } }; -function prepareSizes(sizes: Size | Size[]) { +function prepareSizes(sizes: Size | Size[] | undefined): AsterioBidPayload['sizes'] { if (!Array.isArray(sizes) || sizes.length === 0) { return []; } - const normalizedSizes = typeof sizes[0] === 'number' ? [sizes] : sizes; + const normalizedSizes: Size[] = typeof sizes[0] === 'number' ? [sizes as Size] : sizes as Size[]; return normalizedSizes.map(size => ({ width: size[0], height: size[1] })); } +function getSizes(bidRequest: BidRequest): Size | Size[] | undefined { + return bidRequest.mediaTypes?.banner?.sizes ?? deepAccess(bidRequest, 'sizes'); +} + function getPosition(bidRequest: BidRequest): number | undefined { return bidRequest.params.pos ?? deepAccess(bidRequest, 'mediaTypes.banner.pos') ?? deepAccess(bidRequest, 'mediaTypes.video.pos'); } +function parseNativeAd(ad: string): AsterioNativeBid | undefined { + let parsedResponse: { native?: AsterioNativeResponse }; + try { + parsedResponse = JSON.parse(ad); + } catch (e) { + return; + } + + const nativeResponse = parsedResponse.native; + if (!nativeResponse) { + return; + } + + const native: AsterioNativeBid = { + clickUrl: nativeResponse.link?.url, + clickTrackers: [...(nativeResponse.link?.clicktrackers || [])], + impressionTrackers: [...(nativeResponse.imptrackers || [])], + ortb: nativeResponse + }; + + nativeResponse.eventtrackers?.forEach(tracker => { + if (tracker.event === 1 && tracker.method === 1 && tracker.url) { + native.impressionTrackers.push(tracker.url); + } + }); + + nativeResponse.assets?.forEach(asset => { + if (asset.title?.text) { + native.title = asset.title.text; + } else if (asset.img?.url) { + const image: AsterioNativeImage = { + url: asset.img.url, + width: asset.img.w, + height: asset.img.h + }; + if (asset.img.type === 1) { + native.icon = image; + } else if (asset.img.type === 3 || !native.image) { + native.image = image; + } + } else if (asset.data?.value && (asset.data.type === 2 || !native.body)) { + native.body = asset.data.value; + } + }); + + return native; +} + registerBidder(spec); diff --git a/test/spec/modules/asterioBidAdapter_spec.js b/test/spec/modules/asterioBidAdapter_spec.js index c45ef42dd5..a7f77e497f 100644 --- a/test/spec/modules/asterioBidAdapter_spec.js +++ b/test/spec/modules/asterioBidAdapter_spec.js @@ -65,6 +65,59 @@ const BIDDER_VIDEO_RESPONSE = { }] }; +const BIDDER_NATIVE_RESPONSE = { + bids: [{ + ad: JSON.stringify({ + native: { + assets: [{ + title: { + text: 'Native title' + } + }, { + img: { + type: 3, + url: 'https://cdn.example.com/image.jpg', + w: 300, + h: 250 + } + }, { + img: { + type: 1, + url: 'https://cdn.example.com/icon.jpg', + w: 64, + h: 64 + } + }, { + data: { + type: 2, + value: 'Native body' + } + }], + link: { + url: 'https://advertiser.example.com', + clicktrackers: ['https://tracker.example.com/click'] + }, + eventtrackers: [{ + event: 1, + method: 1, + url: 'https://tracker.example.com/impression' + }] + } + }), + requestId: 'request-3', + cpm: 3.45, + currency: 'USD', + width: 1, + height: 1, + ttl: 300, + creativeId: 'creative-3', + netRevenue: true, + format: 'native', + mediaType: 'native', + adomain: ['native.example.com'] + }] +}; + describe('asterioBidAdapter', function () { const adapter = newBidder(spec); @@ -219,6 +272,30 @@ describe('asterioBidAdapter', function () { expect(result[0].meta.advertiserDomains).to.deep.equal(['video.example.com']); }); + it('should map native bids from direct response', function () { + const result = spec.interpretResponse({ body: BIDDER_NATIVE_RESPONSE }, {}); + + expect(result).to.have.lengthOf(1); + expect(result[0].mediaType).to.equal('native'); + expect(result[0].ad).to.equal(undefined); + expect(result[0].native.title).to.equal('Native title'); + expect(result[0].native.body).to.equal('Native body'); + expect(result[0].native.clickUrl).to.equal('https://advertiser.example.com'); + expect(result[0].native.image).to.deep.equal({ + url: 'https://cdn.example.com/image.jpg', + width: 300, + height: 250 + }); + expect(result[0].native.icon).to.deep.equal({ + url: 'https://cdn.example.com/icon.jpg', + width: 64, + height: 64 + }); + expect(result[0].native.clickTrackers).to.deep.equal(['https://tracker.example.com/click']); + expect(result[0].native.impressionTrackers).to.deep.equal(['https://tracker.example.com/impression']); + expect(result[0].meta.advertiserDomains).to.deep.equal(['native.example.com']); + }); + it('should return empty array for invalid response body', function () { expect(spec.interpretResponse({ body: undefined }, {})).to.deep.equal([]); expect(spec.interpretResponse({ body: '' }, {})).to.deep.equal([]);