Skip to content

Commit

Permalink
new bidder adapter: pangle
Browse files Browse the repository at this point in the history
  • Loading branch information
bytedancer-1 committed Aug 28, 2023
1 parent 4576207 commit 5d93636
Show file tree
Hide file tree
Showing 3 changed files with 334 additions and 0 deletions.
110 changes: 110 additions & 0 deletions modules/pangleBidAdapter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
// ver V1.0.3
import { BANNER } from '../src/mediaTypes.js';
import { ortbConverter } from '../libraries/ortbConverter/converter.js'
import { registerBidder } from '../src/adapters/bidderFactory.js';
import { deepSetValue, generateUUID, timestamp } from '../src/utils.js';
import { getStorageManager } from '../src/storageManager.js';
import { MODULE_TYPE_RTD } from '../src/activities/modules.js';

const BIDDER_CODE = 'pangle';
const ENDPOINT = 'https://pangle.pangleglobal.com/api/ad/union/web_js/common/get_ads';

const DEFAULT_BID_TTL = 30;
const DEFAULT_CURRENCY = 'USD';
const DEFAULT_NET_REVENUE = true;
const PANGLE_COOKIE = '_pangle_id';
const COOKIE_EXP = 86400 * 1000 * 365 * 1; // 1 year
export const storage = getStorageManager({ moduleType: MODULE_TYPE_RTD, moduleName: BIDDER_CODE })

export function isValidUuid(uuid) {
return /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(
uuid
);
}

function getPangleCookieId() {
let sid = storage.cookiesAreEnabled() && storage.getCookie(PANGLE_COOKIE);

if (
!sid || !isValidUuid(sid)
) {
sid = generateUUID();
setPangleCookieId(sid);
}

return sid;
}

function setPangleCookieId(sid) {
if (storage.cookiesAreEnabled()) {
const expires = (new Date(timestamp() + COOKIE_EXP)).toGMTString();

storage.setCookie(PANGLE_COOKIE, sid, expires);
}
}

const converter = ortbConverter({
context: {
netRevenue: DEFAULT_NET_REVENUE,
ttl: DEFAULT_BID_TTL,
currency: DEFAULT_CURRENCY,
mediaType: BANNER
}
});

export const spec = {
code: BIDDER_CODE,
supportedMediaTypes: [BANNER],

getDeviceType: function (ua) {
if ((/ipad|android 3.0|xoom|sch-i800|playbook|tablet|kindle/i.test(ua.toLowerCase()))) {
return 5; // 'tablet'
}
if ((/iphone|ipod|android|blackberry|opera|mini|windows\sce|palm|smartphone|iemobile/i.test(ua.toLowerCase()))) {
return 4; // 'mobile'
}
return 2; // 'desktop'
},

isBidRequestValid: function (bid) {
return !!bid.params.appid && !!bid.params.placementid;
},

buildRequests(bidRequests, bidderRequest) {
const data = converter.toORTB({ bidRequests, bidderRequest })
const devicetype = spec.getDeviceType(navigator.userAgent);
deepSetValue(data, 'device.devicetype', devicetype);
if (bidderRequest.userId && typeof bidderRequest.userId === 'object') {
const pangleId = getPangleCookieId();
// add pangle cookie
const _eids = data.user?.ext?.eids ?? []
deepSetValue(data, 'user.ext.eids', [..._eids, {
source: document.location.host,
uids: [
{
id: pangleId,
atype: 1
}
]
}]);
}
bidRequests.forEach((item, idx) => {
deepSetValue(data.imp[idx], 'ext.networkids', item.params);
deepSetValue(data.imp[idx], 'banner.api', [5]);
});

return [{
method: 'POST',
url: ENDPOINT,
data,
options: { contentType: 'application/json', withCredentials: true }
}]
},

interpretResponse(response, request) {
const bids = converter.fromORTB({ response: response.body, request: request.data }).bids;
return bids;
},
};

registerBidder(spec);
36 changes: 36 additions & 0 deletions modules/pangleBidAdapter.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Overview

Module Name: pangle Bidder Adapter
Module Type: Bidder Adapter
Maintainer: pangle_dsp@bytedance.com

# Description

An adapter to get a bid from pangle DSP.

# Test Parameters
```
var adUnits = [{
// banner
code: 'test1',
mediaTypes: {
banner: {
sizes: [[300, 250]]
}
},
bids: [{
bidder: 'pangle',
params: {
appid: 612,
placementid: 123,
}
}]
}];
```


Where:

* placementid - Placement ID of the ad unit (required)
* appid - app ID of the ad unit (required)
188 changes: 188 additions & 0 deletions test/spec/modules/pangleBidAdapter_spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
import { expect } from 'chai';
import { spec } from 'modules/pangleBidAdapter.js';

const REQUEST = [{
adUnitCode: 'adUnitCode1',
bidId: 'bidId1',
auctionId: 'auctionId-56a2-4f71-9098-720a68f2f708',
ortb2Imp: {
ext: {
tid: 'cccc1234',
}
},
mediaTypes: {
banner: {
sizes: [
[300, 250]
]
}
},
bidder: 'pangle',
params: {
placementid: 999,
appid: 111,
},
},
{
adUnitCode: 'adUnitCode2',
bidId: 'bidId2',
auctionId: 'auctionId-56a2-4f71-9098-720a68f2f708',
ortb2Imp: {
ext: {
tid: 'cccc1234',
}
},
mediaTypes: {
banner: {
sizes: [
[300, 250]
]
}
},
bidder: 'pangle',
params: {
placementid: 999,
appid: 111,
},
}];
const DEFAULT_OPTIONS = {
userId: {
britepoolid: 'pangle-britepool',
criteoId: 'pangle-criteo',
digitrustid: { data: { id: 'pangle-digitrust' } },
id5id: { uid: 'pangle-id5' },
idl_env: 'pangle-idl-env',
lipb: { lipbid: 'pangle-liveintent' },
netId: 'pangle-netid',
parrableId: { eid: 'pangle-parrable' },
pubcid: 'pangle-pubcid',
tdid: 'pangle-ttd',
}
};

const RESPONSE = {
'headers': null,
'body': {
'id': 'requestId',
'seatbid': [
{
'bid': [
{
'id': 'bidId1',
'impid': 'bidId1',
'price': 0.18,
'adm': '<script>adm</script>',
'adid': '144762342',
'adomain': [
'https://dummydomain.com'
],
'iurl': 'iurl',
'cid': '109',
'crid': 'creativeId',
'cat': [],
'w': 300,
'h': 250,
'ext': {
'prebid': {
'type': 'banner'
},
'bidder': {
'pangle': {
'brand_id': 334553,
'auction_id': 514667951122925701,
'bidder_id': 2,
'bid_ad_type': 0
}
}
}
}
],
'seat': 'seat'
}
]
}
};

describe('pangle bid adapter', function () {
describe('isBidRequestValid', function () {
it('should accept request if placementid and appid is passed', function () {
let bid = {
bidder: 'pangle',
params: {
placementid: 999,
appid: 111,
}
};
expect(spec.isBidRequestValid(bid)).to.equal(true);
});
it('reject requests without params', function () {
let bid = {
bidder: 'pangle',
params: {}
};
expect(spec.isBidRequestValid(bid)).to.equal(false);
});
});

describe('buildRequests', function () {
it('creates request data', function () {
let request = spec.buildRequests(REQUEST, DEFAULT_OPTIONS)[0];
expect(request).to.exist.and.to.be.a('object');
const payload = request.data;
expect(payload.imp[0]).to.have.property('id', REQUEST[0].bidId);
expect(payload.imp[1]).to.have.property('id', REQUEST[1].bidId);
});
});

describe('interpretResponse', function () {
it('has bids', function () {
let request = spec.buildRequests(REQUEST, DEFAULT_OPTIONS)[0];
let bids = spec.interpretResponse(RESPONSE, request);
expect(bids).to.be.an('array').that.is.not.empty;
validateBidOnIndex(0);

function validateBidOnIndex(index) {
expect(bids[index]).to.have.property('currency', 'USD');
expect(bids[index]).to.have.property('requestId', RESPONSE.body.seatbid[0].bid[index].id);
expect(bids[index]).to.have.property('cpm', RESPONSE.body.seatbid[0].bid[index].price);
expect(bids[index]).to.have.property('width', RESPONSE.body.seatbid[0].bid[index].w);
expect(bids[index]).to.have.property('height', RESPONSE.body.seatbid[0].bid[index].h);
expect(bids[index]).to.have.property('ad', RESPONSE.body.seatbid[0].bid[index].adm);
expect(bids[index]).to.have.property('creativeId', RESPONSE.body.seatbid[0].bid[index].crid);
expect(bids[index]).to.have.property('ttl', 30);
expect(bids[index]).to.have.property('netRevenue', true);
}
});

it('handles empty response', function () {
let request = spec.buildRequests(REQUEST, DEFAULT_OPTIONS)[0];
const EMPTY_RESP = Object.assign({}, RESPONSE, { 'body': {} });
const bids = spec.interpretResponse(EMPTY_RESP, request);
expect(bids).to.be.empty;
});
});

describe('parseUserAgent', function () {
let desktop, mobile, tablet;
beforeEach(function () {
desktop = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36';
mobile = 'Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Mobile/15E148 Safari/604.1';
tablet = 'Apple iPad: Mozilla/5.0 (iPad; CPU OS 13_5_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.1.1 Mobile/15E148 Safari/605.1.15';
});

it('should return correct device type: tablet', function () {
let deviceType = spec.getDeviceType(tablet);
expect(deviceType).to.equal(5);
});

it('should return correct device type: mobile', function () {
let deviceType = spec.getDeviceType(mobile);
expect(deviceType).to.equal(4);
});

it('should return correct device type: desktop', function () {
let deviceType = spec.getDeviceType(desktop);
expect(deviceType).to.equal(2);
});
});
});

0 comments on commit 5d93636

Please sign in to comment.