Skip to content

Commit

Permalink
PBjs Core : send native targetings for ortb response (prebid#9252)
Browse files Browse the repository at this point in the history
* PBjs Core : send native targetings for ortb response

* Add legacy native properties regardless of response mediaType

* add a test for addLegacyFieldsIfNeeded

* fix lint for test

Co-authored-by: Demetrio Girardi <dgirardi@prebid.org>
  • Loading branch information
2 people authored and jorgeluisrocha committed May 18, 2023
1 parent 75a8ad0 commit eebe3af
Show file tree
Hide file tree
Showing 4 changed files with 193 additions and 78 deletions.
20 changes: 18 additions & 2 deletions src/auction.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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();
}
Expand Down Expand Up @@ -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) => {
Expand Down
51 changes: 31 additions & 20 deletions src/native.js
Original file line number Diff line number Diff line change
Expand Up @@ -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: [
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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);
}

/**
Expand Down Expand Up @@ -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;
Expand All @@ -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 => `<script async src="${url}"></script>`);
if (ortbResponse?.jstracker) { jsTrackers.push(ortbResponse.jstracker); }
if (jsTrackers.length) {
legacyResponse.javascriptTrackers = jsTrackers.join('\n');
}

return legacyResponse;
}

Expand Down
76 changes: 76 additions & 0 deletions test/spec/auctionmanager_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');

Expand Down Expand Up @@ -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, '<script async src="http://www.jstracker.com/file.js"></script>')
});
});
}

describe('getMediaTypeGranularity', function () {
it('video', function () {
let mediaTypes = { video: {id: '1'} };
Expand Down
Loading

0 comments on commit eebe3af

Please sign in to comment.