Skip to content

Commit

Permalink
Eskimi Bid Adapter: Endpoint adjustments and cookie-sync endpoint (#1…
Browse files Browse the repository at this point in the history
…2201)

Co-authored-by: Andrius Versockas <[email protected]>
  • Loading branch information
myDisconnect and Andrius Versockas authored Sep 4, 2024
1 parent fe190c3 commit c2fdee7
Show file tree
Hide file tree
Showing 3 changed files with 193 additions and 27 deletions.
173 changes: 155 additions & 18 deletions modules/eskimiBidAdapter.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,16 @@ import {ortbConverter} from '../libraries/ortbConverter/converter.js';
import {registerBidder} from '../src/adapters/bidderFactory.js';
import {BANNER, VIDEO} from '../src/mediaTypes.js';
import * as utils from '../src/utils.js';
import {getBidIdParameter} from '../src/utils.js';
import {getBidIdParameter, logInfo, mergeDeep} from '../src/utils.js';
import {hasPurpose1Consent} from '../src/utils/gdpr.js';

/**
* @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest
* @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid
* @typedef {import('../src/adapters/bidderFactory.js').validBidRequests} validBidRequests
*/

const BIDDER_CODE = 'eskimi';
const ENDPOINT = 'https://sspback.eskimi.com/bid-request'

const DEFAULT_BID_TTL = 30;
const DEFAULT_CURRENCY = 'USD';
const DEFAULT_NET_REVENUE = true;
Expand All @@ -36,24 +37,39 @@ const VIDEO_ORTB_PARAMS = [

const BANNER_ORTB_PARAMS = [
'battr'
]
];

const REGION_SUBDOMAIN_SUFFIX = {
EU: '',
US: '-us-e',
APAC: '-asia'
};

export const spec = {
code: BIDDER_CODE,
aliases: ['eskimi'],
gvlid: GVLID,
supportedMediaTypes: [BANNER, VIDEO],
isBidRequestValid,
buildRequests,
interpretResponse,
getUserSyncs,
/**
* Register bidder specific code, which will execute if a bid from this bidder won the auction
* @param {Bid} bid The bid that won the auction
*/
onBidWon: function (bid) {
logInfo('Bid won: ', bid);
if (bid.burl) {
utils.triggerPixel(bid.burl);
}
}
},
onTimeout: function (timeoutData) {
logInfo('Timeout: ', timeoutData);
},
onBidderError: function ({error, bidderRequest}) {
logInfo('Error: ', error, bidderRequest);
},
}

registerBidder(spec);
Expand All @@ -79,7 +95,24 @@ const CONVERTER = ortbConverter({
}

return imp;
}
},
request(buildRequest, imps, bidderRequest, context) {
const req = buildRequest(imps, bidderRequest, context);
mergeDeep(req, {
at: 1,
ext: {
pv: '$prebid.version$'
}
})
const bid = context.bidRequests[0];
if (bid.params.coppa) {
utils.deepSetValue(req, 'regs.coppa', 1);
}
if (bid.params.test) {
req.test = 1
}
return req;
},
});

function isBidRequestValid(bidRequest) {
Expand All @@ -101,9 +134,17 @@ function isValidVideoRequest(bidRequest) {
return utils.isArray(videoSizes) && videoSizes.length > 0 && videoSizes.every(size => utils.isNumber(size[0]) && utils.isNumber(size[1]));
}

function buildRequests(validBids, bidderRequest) {
let videoBids = validBids.filter(bid => isVideoBid(bid));
let bannerBids = validBids.filter(bid => isBannerBid(bid));
/**
* Takes an array of valid bid requests, all of which are guaranteed to have passed the isBidRequestValid() test.
* Make a server request from the list of BidRequests.
*
* @param {*} validBidRequests
* @param {*} bidderRequest
* @return ServerRequest Info describing the request to the server.
*/
function buildRequests(validBidRequests, bidderRequest) {
let videoBids = validBidRequests.filter(bid => isVideoBid(bid));
let bannerBids = validBidRequests.filter(bid => isBannerBid(bid));
let requests = [];

bannerBids.forEach(bid => {
Expand All @@ -118,14 +159,14 @@ function buildRequests(validBids, bidderRequest) {
}

function interpretResponse(response, request) {
return CONVERTER.fromORTB({ request: request.data, response: response.body }).bids;
return CONVERTER.fromORTB({request: request.data, response: response.body}).bids;
}

function buildVideoImp(bidRequest, imp) {
const videoAdUnitParams = utils.deepAccess(bidRequest, `mediaTypes.${VIDEO}`, {});
const videoBidderParams = utils.deepAccess(bidRequest, `params.${VIDEO}`, {});

const videoParams = { ...videoAdUnitParams, ...videoBidderParams };
const videoParams = {...videoAdUnitParams, ...videoBidderParams};

const videoSizes = (videoAdUnitParams && videoAdUnitParams.playerSize) || [];

Expand All @@ -144,14 +185,14 @@ function buildVideoImp(bidRequest, imp) {
imp.video.plcmt = imp.video.plcmt || 4;
}

return { ...imp };
return {...imp};
}

function buildBannerImp(bidRequest, imp) {
const bannerAdUnitParams = utils.deepAccess(bidRequest, `mediaTypes.${BANNER}`, {});
const bannerBidderParams = utils.deepAccess(bidRequest, `params.${BANNER}`, {});

const bannerParams = { ...bannerAdUnitParams, ...bannerBidderParams };
const bannerParams = {...bannerAdUnitParams, ...bannerBidderParams};

let sizes = bidRequest.mediaTypes.banner.sizes;

Expand All @@ -166,15 +207,15 @@ function buildBannerImp(bidRequest, imp) {
}
});

return { ...imp };
return {...imp};
}

function createRequest(bidRequests, bidderRequest, mediaType) {
const data = CONVERTER.toORTB({ bidRequests, bidderRequest, context: { mediaType } })
const data = CONVERTER.toORTB({bidRequests, bidderRequest, context: {mediaType}})

const bid = bidRequests.find((b) => b.params.placementId)
if (!data.site) data.site = {}
data.site.ext = { placementId: bid.params.placementId }
data.site.ext = {placementId: bid.params.placementId}

if (bidderRequest.gdprConsent) {
if (!data.user) data.user = {};
Expand All @@ -191,9 +232,9 @@ function createRequest(bidRequests, bidderRequest, mediaType) {

return {
method: 'POST',
url: ENDPOINT,
url: getBidRequestUrlByRegion(),
data: data,
options: { contentType: 'application/json;charset=UTF-8', withCredentials: false }
options: {contentType: 'application/json;charset=UTF-8', withCredentials: false}
}
}

Expand All @@ -204,3 +245,99 @@ function isVideoBid(bid) {
function isBannerBid(bid) {
return utils.deepAccess(bid, 'mediaTypes.banner') || !isVideoBid(bid);
}

/**
* @param syncOptions
* @param responses
* @param gdprConsent
* @param uspConsent
* @param gppConsent
* @return {{type: (string), url: (*|string)}[]}
*/
function getUserSyncs(syncOptions, responses, gdprConsent, uspConsent, gppConsent) {
if ((syncOptions.iframeEnabled || syncOptions.pixelEnabled) && hasSyncConsent(gdprConsent, uspConsent, gppConsent)) {
let pixelType = syncOptions.iframeEnabled ? 'iframe' : 'image';
let query = [];
let syncUrl = getUserSyncUrlByRegion();
// Attaching GDPR Consent Params in UserSync url
if (gdprConsent) {
query.push('gdpr=' + (gdprConsent.gdprApplies & 1));
query.push('gdpr_consent=' + encodeURIComponent(gdprConsent.consentString || ''));
}
// CCPA
if (uspConsent) {
query.push('us_privacy=' + encodeURIComponent(uspConsent));
}
// GPP Consent
if (gppConsent?.gppString && gppConsent?.applicableSections?.length) {
query.push('gpp=' + encodeURIComponent(gppConsent.gppString));
query.push('gpp_sid=' + encodeURIComponent(gppConsent.applicableSections.join(',')));
}
return [{
type: pixelType,
url: `${syncUrl}${query.length > 0 ? '?' + query.join('&') : ''}`
}];
}
}

function hasSyncConsent(gdprConsent, uspConsent, gppConsent) {
return hasPurpose1Consent(gdprConsent) && hasUspConsent(uspConsent) && hasGppConsent(gppConsent);
}

function hasUspConsent(uspConsent) {
return typeof uspConsent !== 'string' || !(uspConsent[0] === '1' && uspConsent[2] === 'Y');
}

function hasGppConsent(gppConsent) {
return (
!(gppConsent && Array.isArray(gppConsent.applicableSections)) ||
gppConsent.applicableSections.every((section) => typeof section === 'number' && section <= 5)
);
}

/**
* Get Bid Request endpoint url by region
* @return {string}
*/
function getBidRequestUrlByRegion() {
return `https://ittr${getRegionSubdomainSuffix()}.eskimi.com/prebidjs`;
}

/**
* Get User Sync endpoint url by region
* @return {string}
*/
function getUserSyncUrlByRegion() {
return `https://ittpx${getRegionSubdomainSuffix()}.eskimi.com/sync?sp_id=137`;
}

/**
* Get subdomain URL suffix by region
* @return {string}
*/
function getRegionSubdomainSuffix() {
try {
const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
const region = timezone.split('/')[0];

switch (region) {
case 'Europe':
case 'Africa':
case 'Atlantic':
case 'Arctic':
return REGION_SUBDOMAIN_SUFFIX['EU'];
case 'Asia':
case 'Australia':
case 'Antarctica':
case 'Pacific':
case 'Indian':
return REGION_SUBDOMAIN_SUFFIX['APAC'];
case 'America':
return REGION_SUBDOMAIN_SUFFIX['US'];
default:
return REGION_SUBDOMAIN_SUFFIX['EU'];
}
} catch (err) {
return REGION_SUBDOMAIN_SUFFIX['EU'];
}
}
33 changes: 31 additions & 2 deletions modules/eskimiBidAdapter.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,35 @@ Banner and video formats are supported.

Where:

* placementId - Placement ID of the ad unit (required)
* bcat, badv, bapp, battr - ORTB blocking parameters as specified by OpenRTB 2.5
* `placementId` - Placement ID of the ad unit (required)
* `bcat`, `badv`, `bapp`, `battr` - ORTB blocking parameters as specified by OpenRTB 2.5

# ## Configuration

Eskimi recommends the UserSync configuration below. Without it, the Eskimi adapter will not able to perform user syncs, which lowers match rate and reduces monetization.

```javascript
pbjs.setConfig({
userSync: {
filterSettings: {
iframe: {
bidders: ['eskimi'],
filter: 'include'
}
},
syncDelay: 6000
}});
```

### Bidder Settings

The Eskimi bid adapter uses browser local storage. Since Prebid.js 7.x, the access to it must be explicitly set.

```js
// https://docs.prebid.org/dev-docs/publisher-api-reference/bidderSettings.html
pbjs.bidderSettings = {
eskimi: {
storageAllowed: true
}
}
```
14 changes: 7 additions & 7 deletions test/spec/modules/eskimiBidAdapter_spec.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { expect } from 'chai';
import { spec } from 'modules/eskimiBidAdapter.js';
import {expect} from 'chai';
import {spec} from 'modules/eskimiBidAdapter.js';
import * as utils from 'src/utils';

const BANNER_BID = {
Expand Down Expand Up @@ -165,8 +165,8 @@ describe('Eskimi bid adapter', function () {
it('should properly forward ORTB blocking params', function () {
let bid = utils.deepClone(BANNER_BID);
bid = utils.mergeDeep(bid, {
params: { bcat: ['IAB1-1'], badv: ['example.com'], bapp: ['com.example'] },
mediaTypes: { banner: { battr: [1] } }
params: {bcat: ['IAB1-1'], badv: ['example.com'], bapp: ['com.example']},
mediaTypes: {banner: {battr: [1]}}
});

let [request] = spec.buildRequests([bid], BIDDER_REQUEST);
Expand Down Expand Up @@ -253,7 +253,7 @@ describe('Eskimi bid adapter', function () {
const [request] = spec.buildRequests([bid], BIDDER_REQUEST);
const response = utils.deepClone(BANNER_BID_RESPONSE);

const bids = spec.interpretResponse({ body: response }, request);
const bids = spec.interpretResponse({body: response}, request);
expect(bids).to.be.an('array').that.is.not.empty;

expect(bids[0].mediaType).to.equal('banner');
Expand All @@ -274,7 +274,7 @@ describe('Eskimi bid adapter', function () {
const bid = utils.deepClone(BANNER_BID);

let request = spec.buildRequests([bid], BIDDER_REQUEST)[0];
const EMPTY_RESP = Object.assign({}, BANNER_BID_RESPONSE, { 'body': {} });
const EMPTY_RESP = Object.assign({}, BANNER_BID_RESPONSE, {'body': {}});
const bids = spec.interpretResponse(EMPTY_RESP, request);
expect(bids).to.be.empty;
});
Expand All @@ -285,7 +285,7 @@ describe('Eskimi bid adapter', function () {
const bid = utils.deepClone(VIDEO_BID);

const [request] = spec.buildRequests([bid], BIDDER_REQUEST);
const bids = spec.interpretResponse({ body: VIDEO_BID_RESPONSE }, request);
const bids = spec.interpretResponse({body: VIDEO_BID_RESPONSE}, request);
expect(bids).to.be.an('array').that.is.not.empty;

expect(bids[0].mediaType).to.equal('video');
Expand Down

0 comments on commit c2fdee7

Please sign in to comment.