Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion modules/asterioBidAdapter.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
115 changes: 110 additions & 5 deletions modules/asterioBidAdapter.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -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<typeof BIDDER_CODE> = {
code: BIDDER_CODE,
supportedMediaTypes: [BANNER, VIDEO],
supportedMediaTypes: [BANNER, VIDEO, NATIVE],

isBidRequestValid: function (bid) {
return !!(bid.params && bid.params.adUnitToken);
Expand All @@ -54,7 +99,7 @@ export const spec: BidderSpec<typeof BIDDER_CODE> = {
bidId: bidRequest.bidId,
adUnitToken: bidRequest.params.adUnitToken,
pos: getPosition(bidRequest),
sizes: prepareSizes(bidRequest.sizes)
sizes: prepareSizes(getSizes(bidRequest))
}));
Comment on lines 99 to 103
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Include native asset request in outbound bid payload

Native support is declared, but the outbound bid payload only includes bidId, adUnitToken, pos, and sizes; it never forwards the ad unit’s native asset request (mediaTypes.native / ORTB native assets). In ORTB-native setups with custom asset IDs or required assets, the bidder cannot reliably match requested assets, which can cause returned native bids to fail core native validation and be dropped before auction completion.

Useful? React with 👍 / 👎.


const payload: {
Expand Down Expand Up @@ -117,6 +162,14 @@ export const spec: BidderSpec<typeof BIDDER_CODE> = {
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 || [];

Expand All @@ -134,16 +187,68 @@ export const spec: BidderSpec<typeof BIDDER_CODE> = {
}
};

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<typeof BIDDER_CODE>): Size | Size[] | undefined {
return bidRequest.mediaTypes?.banner?.sizes ?? deepAccess(bidRequest, 'sizes');
}

function getPosition(bidRequest: BidRequest<typeof BIDDER_CODE>): 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;
Comment on lines +246 to +247
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Map only description data assets into native body

The body assignment currently falls back to the first data asset regardless of type via (asset.data.type === 2 || !native.body). If a non-description data asset (for example sponsored-by/type 1) appears first and no type-2 asset is present, native.body is populated with the wrong value, causing incorrect native content rendering.

Useful? React with 👍 / 👎.

}
});

return native;
}

registerBidder(spec);
77 changes: 77 additions & 0 deletions test/spec/modules/asterioBidAdapter_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down Expand Up @@ -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([]);
Expand Down
Loading