diff --git a/src/auction.js b/src/auction.js
index 09fb275afdb..01553b2d25a 100644
--- a/src/auction.js
+++ b/src/auction.js
@@ -76,7 +76,7 @@ import {
timestamp
} from './utils.js';
import {getPriceBucketString} from './cpmBucketManager.js';
-import {getNativeTargeting} from './native.js';
+import {getNativeTargeting, toLegacyResponse} from './native.js';
import {getCacheUrl, store} from './videoCache.js';
import {Renderer} from './Renderer.js';
import {config} from './config.js';
@@ -462,9 +462,14 @@ export function auctionCallbacks(auctionDone, auctionInstance, {index = auctionM
handleBidResponse(adUnitCode, bid, (done) => {
let bidResponse = getPreparedBidForAuction(bid);
- if (bidResponse.mediaType === 'video') {
+ if (bidResponse.mediaType === VIDEO) {
tryAddVideoBid(auctionInstance, bidResponse, done);
} else {
+ if (FEATURES.NATIVE && bidResponse.native != null && typeof bidResponse.native === 'object') {
+ // NOTE: augment bidResponse.native even if bidResponse.mediaType !== NATIVE; it's possible
+ // to treat banner responses as native
+ addLegacyFieldsIfNeeded(bidResponse);
+ }
addBidToAuction(auctionInstance, bidResponse);
done();
}
@@ -593,6 +598,17 @@ function tryAddVideoBid(auctionInstance, bidResponse, afterBidAdded, {index = au
}
}
+// Native bid response might be in ortb2 format - adds legacy field for backward compatibility
+const addLegacyFieldsIfNeeded = (bidResponse) => {
+ const nativeOrtbRequest = auctionManager.index.getAdUnit(bidResponse)?.nativeOrtbRequest;
+ const nativeOrtbResponse = bidResponse.native?.ortb
+
+ if (nativeOrtbRequest && nativeOrtbResponse) {
+ const legacyResponse = toLegacyResponse(nativeOrtbResponse, nativeOrtbRequest);
+ Object.assign(bidResponse.native, legacyResponse);
+ }
+}
+
const storeInCache = (batch) => {
store(batch.map(entry => entry.bidResponse), function (error, cacheIds) {
cacheIds.forEach((cacheId, i) => {
diff --git a/src/native.js b/src/native.js
index 022ece457f5..25f8c38cb30 100644
--- a/src/native.js
+++ b/src/native.js
@@ -23,7 +23,7 @@ export const NATIVE_TARGETING_KEYS = Object.keys(CONSTANTS.NATIVE_KEYS).map(
key => CONSTANTS.NATIVE_KEYS[key]
);
-const IMAGE = {
+export const IMAGE = {
ortb: {
ver: '1.2',
assets: [
@@ -385,26 +385,14 @@ export function getNativeTargeting(bid, {index = auctionManager.index} = {}) {
return keyValues;
}
-const getNativeRequest = (bidResponse) => auctionManager.index.getAdUnit(bidResponse)?.nativeOrtbRequest;
-
-function assetsMessage(data, adObject, keys, {getNativeReq = getNativeRequest} = {}) {
+function assetsMessage(data, adObject, keys) {
const message = {
message: 'assetResponse',
adId: data.adId,
};
- // Pass to Prebid Universal Creative all assets, the legacy ones + the ortb ones (under ortb property)
- const ortbRequest = getNativeReq(adObject);
let nativeResp = adObject.native;
- const ortbResponse = adObject.native?.ortb;
- let legacyResponse = {};
- if (ortbRequest && ortbResponse) {
- legacyResponse = toLegacyResponse(ortbResponse, ortbRequest);
- nativeResp = {
- ...adObject.native,
- ...legacyResponse
- };
- }
+
if (adObject.native.ortb) {
message.ortb = adObject.native.ortb;
}
@@ -435,13 +423,13 @@ function assetsMessage(data, adObject, keys, {getNativeReq = getNativeRequest} =
* Constructs a message object containing asset values for each of the
* requested data keys.
*/
-export function getAssetMessage(data, adObject, {getNativeReq = getNativeRequest} = {}) {
+export function getAssetMessage(data, adObject) {
const keys = data.assets.map((k) => getKeyByValue(CONSTANTS.NATIVE_KEYS, k));
- return assetsMessage(data, adObject, keys, {getNativeReq});
+ return assetsMessage(data, adObject, keys);
}
-export function getAllAssetsMessage(data, adObject, {getNativeReq = getNativeRequest} = {}) {
- return assetsMessage(data, adObject, null, {getNativeReq});
+export function getAllAssetsMessage(data, adObject) {
+ return assetsMessage(data, adObject, null);
}
/**
@@ -749,7 +737,7 @@ export function toOrtbNativeResponse(legacyResponse, ortbRequest) {
* @param {*} ortbRequest the ortb request, useful to match ids.
* @returns an object containing the response in legacy native format: { title: "this is a title", image: ... }
*/
-function toLegacyResponse(ortbResponse, ortbRequest) {
+export function toLegacyResponse(ortbResponse, ortbRequest) {
const legacyResponse = {};
const requestAssets = ortbRequest?.assets || [];
legacyResponse.clickUrl = ortbResponse.link.url;
@@ -764,6 +752,29 @@ function toLegacyResponse(ortbResponse, ortbRequest) {
legacyResponse[PREBID_NATIVE_DATA_KEYS_TO_ORTB_INVERSE[NATIVE_ASSET_TYPES_INVERSE[requestAsset.data.type]]] = asset.data.value;
}
}
+
+ // Handle trackers
+ legacyResponse.impressionTrackers = [];
+ let jsTrackers = [];
+
+ if (ortbRequest?.imptrackers) {
+ legacyResponse.impressionTrackers.push(...ortbRequest.imptrackers);
+ }
+ for (const eventTracker of ortbResponse?.eventtrackers || []) {
+ if (eventTracker.event === TRACKER_EVENTS.impression && eventTracker.method === TRACKER_METHODS.img) {
+ legacyResponse.impressionTrackers.push(eventTracker.url);
+ }
+ if (eventTracker.event === TRACKER_EVENTS.impression && eventTracker.method === TRACKER_METHODS.js) {
+ jsTrackers.push(eventTracker.url);
+ }
+ }
+
+ jsTrackers = jsTrackers.map(url => ``);
+ if (ortbResponse?.jstracker) { jsTrackers.push(ortbResponse.jstracker); }
+ if (jsTrackers.length) {
+ legacyResponse.javascriptTrackers = jsTrackers.join('\n');
+ }
+
return legacyResponse;
}
diff --git a/test/spec/auctionmanager_spec.js b/test/spec/auctionmanager_spec.js
index 47bdd4a3772..ba5f7c7cb80 100644
--- a/test/spec/auctionmanager_spec.js
+++ b/test/spec/auctionmanager_spec.js
@@ -22,6 +22,7 @@ import 'modules/debugging/index.js' // some tests look for debugging side effect
import {AuctionIndex} from '../../src/auctionIndex.js';
import {expect} from 'chai';
import {deepClone} from '../../src/utils.js';
+import { IMAGE as ortbNativeRequest } from 'src/native.js';
var assert = require('assert');
@@ -1276,6 +1277,81 @@ describe('auctionmanager.js', function () {
});
});
+ if (FEATURES.NATIVE) {
+ describe('addBidResponse native', function () {
+ let makeRequestsStub;
+ let ajaxStub;
+ let spec;
+ let auction;
+
+ beforeEach(function () {
+ makeRequestsStub = sinon.stub(adapterManager, 'makeBidRequests');
+ ajaxStub = sinon.stub(ajaxLib, 'ajaxBuilder').callsFake(mockAjaxBuilder);
+
+ const adUnits = [{
+ code: ADUNIT_CODE,
+ transactionId: ADUNIT_CODE,
+ bids: [
+ {bidder: BIDDER_CODE, params: {placementId: 'id'}},
+ ],
+ nativeOrtbRequest: ortbNativeRequest.ortb,
+ mediaTypes: { native: { type: 'image' } }
+ }];
+ auction = auctionModule.newAuction({adUnits, adUnitCodes: [ADUNIT_CODE], callback: function() {}, cbTimeout: 3000});
+ indexAuctions = [auction];
+ const createAuctionStub = sinon.stub(auctionModule, 'newAuction');
+ createAuctionStub.returns(auction);
+
+ spec = mockBidder(BIDDER_CODE);
+ registerBidder(spec);
+ });
+
+ afterEach(function () {
+ ajaxStub.restore();
+ auctionModule.newAuction.restore();
+ adapterManager.makeBidRequests.restore();
+ });
+
+ it('should add legacy fields to native response', function () {
+ let nativeBid = mockBid();
+ nativeBid.mediaType = 'native';
+ nativeBid.native = {
+ ortb: {
+ ver: '1.2',
+ assets: [
+ { id: 2, title: { text: 'Sample title' } },
+ { id: 4, data: { value: 'Sample body' } },
+ { id: 3, data: { value: 'Sample sponsoredBy' } },
+ { id: 1, img: { url: 'https://www.example.com/image.png', w: 200, h: 200 } },
+ { id: 5, img: { url: 'https://www.example.com/icon.png', w: 32, h: 32 } }
+ ],
+ link: { url: 'http://www.click.com' },
+ eventtrackers: [
+ { event: 1, method: 1, url: 'http://www.imptracker.com' },
+ { event: 1, method: 2, url: 'http://www.jstracker.com/file.js' }
+ ]
+ }
+ }
+
+ let bidRequest = mockBidRequest(nativeBid, { mediaType: { native: ortbNativeRequest } });
+ makeRequestsStub.returns([bidRequest]);
+
+ spec.interpretResponse.returns(nativeBid);
+ auction.callBids();
+
+ const addedBid = auction.getBidsReceived().pop();
+ assert.equal(addedBid.native.body, 'Sample body')
+ assert.equal(addedBid.native.title, 'Sample title')
+ assert.equal(addedBid.native.sponsoredBy, 'Sample sponsoredBy')
+ assert.equal(addedBid.native.clickUrl, 'http://www.click.com')
+ assert.equal(addedBid.native.image, 'https://www.example.com/image.png')
+ assert.equal(addedBid.native.icon, 'https://www.example.com/icon.png')
+ assert.equal(addedBid.native.impressionTrackers[0], 'http://www.imptracker.com')
+ assert.equal(addedBid.native.javascriptTrackers, '')
+ });
+ });
+ }
+
describe('getMediaTypeGranularity', function () {
it('video', function () {
let mediaTypes = { video: {id: '1'} };
diff --git a/test/spec/native_spec.js b/test/spec/native_spec.js
index 23350781a3d..2b7c2b88449 100644
--- a/test/spec/native_spec.js
+++ b/test/spec/native_spec.js
@@ -5,6 +5,7 @@ import {
nativeBidIsValid,
getAssetMessage,
getAllAssetsMessage,
+ toLegacyResponse,
decorateAdUnitsWithNativeParams,
isOpenRTBBidRequestValid,
isNativeOpenRTBBidValid,
@@ -37,6 +38,7 @@ const bid = {
clickTrackers: ['https://tracker.example'],
impressionTrackers: ['https://impression.example'],
javascriptTrackers: '',
+ privacyLink: 'https://privacy-link.example',
ext: {
foo: 'foo-value',
baz: 'baz-value',
@@ -98,9 +100,18 @@ const ortbBid = {
privacy: 'https://privacy-link.example',
ver: '1.2'
}
- },
+ }
};
+const completeNativeBid = {
+ adId: '123',
+ transactionId: 'au',
+ native: {
+ ...bid.native,
+ ...ortbBid.native
+ }
+}
+
const ortbRequest = {
assets: [
{
@@ -224,6 +235,14 @@ describe('native.js', function () {
expect(targeting.hb_native_baz).to.equal('hb_native_baz:123');
});
+ it('sends placeholdes targetings with ortb native response', function () {
+ const targeting = getNativeTargeting(completeNativeBid);
+
+ expect(targeting[CONSTANTS.NATIVE_KEYS.title]).to.equal('Native Creative');
+ expect(targeting[CONSTANTS.NATIVE_KEYS.body]).to.equal('Cool description great stuff');
+ expect(targeting[CONSTANTS.NATIVE_KEYS.clickUrl]).to.equal('https://www.link.example');
+ });
+
it('should only include native targeting keys with values', function () {
const adUnit = {
transactionId: 'au',
@@ -302,6 +321,10 @@ describe('native.js', function () {
required: false,
sendTargetingKeys: false,
},
+ privacyLink: {
+ required: false,
+ sendTargetingKeys: false,
+ },
ext: {
foo: {
required: false,
@@ -348,6 +371,7 @@ describe('native.js', function () {
CONSTANTS.NATIVE_KEYS.icon,
CONSTANTS.NATIVE_KEYS.sponsoredBy,
CONSTANTS.NATIVE_KEYS.clickUrl,
+ CONSTANTS.NATIVE_KEYS.privacyLink,
CONSTANTS.NATIVE_KEYS.rendererUrl,
]);
@@ -380,6 +404,7 @@ describe('native.js', function () {
CONSTANTS.NATIVE_KEYS.icon,
CONSTANTS.NATIVE_KEYS.sponsoredBy,
CONSTANTS.NATIVE_KEYS.clickUrl,
+ CONSTANTS.NATIVE_KEYS.privacyLink,
]);
expect(bid.native.adTemplate).to.deep.equal(
@@ -437,9 +462,9 @@ describe('native.js', function () {
adId: '123',
};
- const message = getAllAssetsMessage(messageRequest, bid, {getNativeReq: () => null});
+ const message = getAllAssetsMessage(messageRequest, bid);
- expect(message.assets.length).to.equal(9);
+ expect(message.assets.length).to.equal(10);
expect(message.assets).to.deep.include({
key: 'body',
value: bid.native.body,
@@ -485,7 +510,7 @@ describe('native.js', function () {
adId: '123',
};
- const message = getAllAssetsMessage(messageRequest, bidWithUndefinedFields, {getNativeReq: () => null});
+ const message = getAllAssetsMessage(messageRequest, bidWithUndefinedFields);
expect(message.assets.length).to.equal(4);
expect(message.assets).to.deep.include({
@@ -506,16 +531,16 @@ describe('native.js', function () {
});
});
- it('creates native all asset message with OpenRTB format', function () {
+ it('creates native all asset message with complete format', function () {
const messageRequest = {
message: 'Prebid Native',
action: 'allAssetRequest',
adId: '123',
};
- const message = getAllAssetsMessage(messageRequest, ortbBid, {getNativeReq: () => ortbRequest});
+ const message = getAllAssetsMessage(messageRequest, completeNativeBid);
- expect(message.assets.length).to.equal(8);
+ expect(message.assets.length).to.equal(10);
expect(message.assets).to.deep.include({
key: 'body',
value: bid.native.body,
@@ -548,6 +573,14 @@ describe('native.js', function () {
key: 'privacyLink',
value: ortbBid.native.ortb.privacy,
});
+ expect(message.assets).to.deep.include({
+ key: 'foo',
+ value: bid.native.ext.foo,
+ });
+ expect(message.assets).to.deep.include({
+ key: 'baz',
+ value: bid.native.ext.baz,
+ });
});
const SAMPLE_ORTB_REQUEST = toOrtbNativeRequest({
@@ -555,60 +588,39 @@ describe('native.js', function () {
body: 'vbody'
});
const SAMPLE_ORTB_RESPONSE = {
- native: {
- ortb: {
- link: {
- url: 'url'
- },
- assets: [
- {
- id: 0,
- title: {
- text: 'vtitle'
- }
- },
- {
- id: 1,
- data: {
- value: 'vbody'
- }
- }
- ]
+ link: {
+ url: 'url'
+ },
+ assets: [
+ {
+ id: 0,
+ title: {
+ text: 'vtitle'
+ }
+ },
+ {
+ id: 1,
+ data: {
+ value: 'vbody'
+ }
}
- }
+ ],
+ eventtrackers: [
+ { event: 1, method: 1, url: 'https://sampleurl.com' },
+ { event: 1, method: 2, url: 'https://sampleurljs.com' }
+ ]
}
- describe('getAllAssetsMessage', () => {
+ describe('toLegacyResponse', () => {
it('returns assets in legacy format for ortb responses', () => {
- const actual = getAllAssetsMessage({}, SAMPLE_ORTB_RESPONSE, {getNativeReq: () => SAMPLE_ORTB_REQUEST});
- expect(actual.assets).to.eql([
- {
- key: 'clickUrl',
- value: 'url'
- },
- {
- key: 'title',
- value: 'vtitle'
- },
- {
- key: 'body',
- value: 'vbody'
- },
- ])
+ const actual = toLegacyResponse(SAMPLE_ORTB_RESPONSE, SAMPLE_ORTB_REQUEST);
+ expect(actual.body).to.equal('vbody');
+ expect(actual.title).to.equal('vtitle');
+ expect(actual.clickUrl).to.equal('url');
+ expect(actual.javascriptTrackers).to.equal('');
+ expect(actual.impressionTrackers.length).to.equal(1);
+ expect(actual.impressionTrackers[0]).to.equal('https://sampleurl.com');
});
});
- describe('getAssetsMessage', () => {
- Object.entries({
- 'hb_native_title': {key: 'title', value: 'vtitle'},
- 'hb_native_body': {key: 'body', value: 'vbody'}
- }).forEach(([tkey, assetVal]) => {
- it(`returns ${tkey} asset in legacy format for ortb responses`, () => {
- const actual = getAssetMessage({
- assets: [tkey]
- }, SAMPLE_ORTB_RESPONSE, {getNativeReq: () => SAMPLE_ORTB_REQUEST})
- expect(actual.assets).to.eql([assetVal])
- })
- })
- })
});
describe('validate native openRTB', function () {