Skip to content

Commit

Permalink
Flipp Bid Adapter : initial release (prebid#10412)
Browse files Browse the repository at this point in the history
* Flipp Bid Adapter: initial release

* Added flippBidAdapter

* OFF-372 Support DTX/Hero in flippBidAdapter (#2)

* support creativeType

* OFF-422 flippBidAdapter handle AdTypes

---------

Co-authored-by: Jairo Panduro <jpanduro@blackbird-lab.com>

* OFF-465 Add getUserKey logic to prebid.js adapter (#3)

* Support cookie sync and uid

* address pr feedback

* remove redundant check

* OFF-500 Support "startCompact" param for Prebid.JS #4

* set startCompact default value (#5)

* fix docs

* use client bidding endpoint

* update unit testing endpoint

---------

Co-authored-by: Jairo Panduro <jpanduro@blackbird-lab.com>
  • Loading branch information
mike-lei and jpanduro-blackbird authored Sep 11, 2023
1 parent 5c9e0cf commit e626373
Show file tree
Hide file tree
Showing 3 changed files with 397 additions and 0 deletions.
183 changes: 183 additions & 0 deletions modules/flippBidAdapter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
import {isEmpty, parseUrl} from '../src/utils.js';
import { registerBidder } from '../src/adapters/bidderFactory.js';
import { BANNER } from '../src/mediaTypes.js';
import {getStorageManager} from '../src/storageManager.js';

const NETWORK_ID = 11090;
const AD_TYPES = [4309, 641];
const DTX_TYPES = [5061];
const TARGET_NAME = 'inline';
const BIDDER_CODE = 'flipp';
const ENDPOINT = 'https://gateflipp.flippback.com/flyer-locator-service/client_bidding';
const DEFAULT_TTL = 30;
const DEFAULT_CURRENCY = 'USD';
const DEFAULT_CREATIVE_TYPE = 'NativeX';
const VALID_CREATIVE_TYPES = ['DTX', 'NativeX'];
const FLIPP_USER_KEY = 'flipp-uid';
const COMPACT_DEFAULT_HEIGHT = 600;

let userKey = null;
export const storage = getStorageManager({bidderCode: BIDDER_CODE});

export function getUserKey(options = {}) {
if (userKey) {
return userKey;
}

// If the partner provides the user key use it, otherwise fallback to cookies
if (options.userKey && isValidUserKey(options.userKey)) {
userKey = options.userKey;
return options.userKey;
}
// Grab from Cookie
const foundUserKey = storage.cookiesAreEnabled() && storage.getCookie(FLIPP_USER_KEY);
if (foundUserKey) {
return foundUserKey;
}

// Generate if none found
userKey = generateUUID();

// Set cookie
if (storage.cookiesAreEnabled()) {
storage.setCookie(FLIPP_USER_KEY, userKey);
}

return userKey;
}

function isValidUserKey(userKey) {
return !userKey.startsWith('#');
}

const generateUUID = () => {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => {
const r = (Math.random() * 16) | 0;
const v = c === 'x' ? r : (r & 0x3) | 0x8;
return v.toString(16);
});
};

/**
* Determines if a creativeType is valid
*
* @param {string} creativeType The Creative Type to validate.
* @return string creativeType if this is a valid Creative Type, and 'NativeX' otherwise.
*/
const validateCreativeType = (creativeType) => {
if (creativeType && VALID_CREATIVE_TYPES.includes(creativeType)) {
return creativeType;
} else {
return DEFAULT_CREATIVE_TYPE;
}
};

const getAdTypes = (creativeType) => {
if (creativeType === 'DTX') {
return DTX_TYPES;
}
return AD_TYPES;
}

export const spec = {
code: BIDDER_CODE,
supportedMediaTypes: [BANNER],
/**
* Determines whether or not the given bid request is valid.
*
* @param {BidRequest} bid The bid params to validate.
* @return boolean True if this is a valid bid, and false otherwise.
*/
isBidRequestValid: function(bid) {
return !!(bid.params.siteId) && !!(bid.params.publisherNameIdentifier);
},
/**
* Make a server request from the list of BidRequests.
*
* @param {BidRequest[]} validBidRequests[] an array of bids
* @param {BidderRequest} bidderRequest master bidRequest object
* @return ServerRequest Info describing the request to the server.
*/
buildRequests: function(validBidRequests, bidderRequest) {
const urlParams = parseUrl(bidderRequest.refererInfo.page).search;
const contentCode = urlParams['flipp-content-code'];
const userKey = getUserKey(validBidRequests[0]?.params);
const placements = validBidRequests.map((bid, index) => {
const options = bid.params.options || {};
if (!options.hasOwnProperty('startCompact')) {
options.startCompact = true;
}
return {
divName: TARGET_NAME,
networkId: NETWORK_ID,
siteId: bid.params.siteId,
adTypes: getAdTypes(bid.params.creativeType),
count: 1,
...(!isEmpty(bid.params.zoneIds) && {zoneIds: bid.params.zoneIds}),
properties: {
...(!isEmpty(contentCode) && {contentCode: contentCode.slice(0, 32)}),
},
options,
prebid: {
requestId: bid.bidId,
publisherNameIdentifier: bid.params.publisherNameIdentifier,
height: bid.mediaTypes.banner.sizes[index][0],
width: bid.mediaTypes.banner.sizes[index][1],
creativeType: validateCreativeType(bid.params.creativeType),
}
}
});
return {
method: 'POST',
url: ENDPOINT,
data: {
placements,
url: bidderRequest.refererInfo.page,
user: {
key: userKey,
},
},
}
},
/**
* Unpack the response from the server into a list of bids.
*
* @param {ServerResponse} serverResponse A successful response from the server.
* @param {BidRequest} bidRequest A bid request object
* @return {Bid[]} An array of bids which were nested inside the server.
*/
interpretResponse: function(serverResponse, bidRequest) {
if (!serverResponse?.body) return [];
const placements = bidRequest.data.placements;
const res = serverResponse.body;
if (!isEmpty(res) && !isEmpty(res.decisions) && !isEmpty(res.decisions.inline)) {
return res.decisions.inline.map(decision => {
const placement = placements.find(p => p.prebid.requestId === decision.prebid?.requestId);
const height = placement.options?.startCompact ? COMPACT_DEFAULT_HEIGHT : decision.height;
return {
bidderCode: BIDDER_CODE,
requestId: decision.prebid?.requestId,
cpm: decision.prebid?.cpm,
width: decision.width,
height,
creativeId: decision.adId,
currency: DEFAULT_CURRENCY,
netRevenue: true,
ttl: DEFAULT_TTL,
ad: decision.prebid?.creative,
}
});
}
return [];
},

/**
* Register the user sync pixels which should be dropped after the auction.
*
* @param {SyncOptions} syncOptions Which user syncs are allowed?
* @param {ServerResponse[]} serverResponses List of server's responses.
* @return {UserSync[]} The user syncs which should be dropped.
*/
getUserSyncs: (syncOptions, serverResponses) => [],
}
registerBidder(spec);
44 changes: 44 additions & 0 deletions modules/flippBidAdapter.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Overview

```
Module Name: Flipp Bid Adapter
Module Type: Bidder Adapter
Maintainer: prebid@flipp.com
```

# Description

This module connects publishers to Flipp's Shopper Experience via Prebid.js.


# Test parameters

```javascript
var adUnits = [
{
code: 'flipp-scroll-ad-content',
mediaTypes: {
banner: {
sizes: [
[300, 600]
]
}
},
bids: [
{
bidder: 'flipp',
params: {
creativeType: 'NativeX', // Optional, can be one of 'NativeX' (default) or 'DTX'
publisherNameIdentifier: 'wishabi-test-publisher', // Required
siteId: 1192075, // Required
zoneIds: [260678], // Optional
userKey: "", // Optional
options: {
startCompact: true // Optional, default to true
}
}
}
]
}
]
```
170 changes: 170 additions & 0 deletions test/spec/modules/flippBidAdapter_spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
import {expect} from 'chai';
import {spec} from 'modules/flippBidAdapter';
import {newBidder} from 'src/adapters/bidderFactory';
const ENDPOINT = 'https://gateflipp.flippback.com/flyer-locator-service/client_bidding';
describe('flippAdapter', function () {
const adapter = newBidder(spec);

describe('inherited functions', function () {
it('exists and is a function', function () {
expect(adapter.callBids).to.exist.and.to.be.a('function');
});
});

describe('isBidRequestValid', function () {
const bid = {
bidder: 'flipp',
params: {
publisherNameIdentifier: 'random',
siteId: 1234,
zoneIds: [1, 2, 3, 4],
}
};
it('should return true when required params found', function () {
expect(spec.isBidRequestValid(bid)).to.equal(true);
});

it('should return false when required params are not passed', function () {
let invalidBid = Object.assign({}, bid);
invalidBid.params = { siteId: 1234 }
expect(spec.isBidRequestValid(invalidBid)).to.equal(false);
});
});

describe('buildRequests', function () {
const bidRequests = [{
bidder: 'flipp',
params: {
siteId: 1234,
},
adUnitCode: '/10000/unit_code',
sizes: [[300, 600]],
mediaTypes: {banner: {sizes: [[300, 600]]}},
bidId: '237f4d1a293f99',
bidderRequestId: '1a857fa34c1c96',
auctionId: 'a297d1aa-7900-4ce4-a0aa-caa8d46c4af7',
transactionId: '00b2896c-2731-4f01-83e4-7a3ad5da13b6',
}];
const bidderRequest = {
refererInfo: {
referer: 'http://example.com'
}
};

it('sends bid request to ENDPOINT via POST', function () {
const request = spec.buildRequests(bidRequests, bidderRequest);
expect(request.method).to.equal('POST');
});

it('sends bid request to ENDPOINT with query parameter', function () {
const request = spec.buildRequests(bidRequests, bidderRequest);
expect(request.url).to.equal(ENDPOINT);
});
});

describe('interpretResponse', function() {
it('should get correct bid response', function() {
const bidRequest = {
method: 'POST',
url: ENDPOINT,
data: {
placements: [{
divName: 'slot',
networkId: 12345,
siteId: 12345,
adTypes: [12345],
count: 1,
prebid: {
requestId: '237f4d1a293f99',
publisherNameIdentifier: 'bid.params.publisherNameIdentifier',
height: 600,
width: 300,
},
user: '10462725-da61-4d3a-beff-6d05239e9a6e"',
}],
url: 'http://example.com',
},
};

const serverResponse = {
body: {
'decisions': {
'inline': [{
'bidCpm': 1,
'adId': 262838368,
'height': 600,
'width': 300,
'storefront': { 'flyer_id': 5435567 },
'prebid': {
'requestId': '237f4d1a293f99',
'cpm': 1.11,
'creative': 'Returned from server',
}
}]
},
'location': {'city': 'Oakville'},
},
};

const expectedResponse = [
{
bidderCode: 'flipp',
requestId: '237f4d1a293f99',
currency: 'USD',
cpm: 1.11,
netRevenue: true,
width: 300,
height: 600,
creativeId: 262838368,
ttl: 30,
ad: 'Returned from server',
}
];

const result = spec.interpretResponse(serverResponse, bidRequest);
expect(result).to.have.lengthOf(1);
expect(result).to.deep.have.same.members(expectedResponse);
});

it('should get empty bid response when no ad is returned', function() {
const bidRequest = {
method: 'POST',
url: ENDPOINT,
data: {
placements: [{
divName: 'slot',
networkId: 12345,
siteId: 12345,
adTypes: [12345],
count: 1,
prebid: {
requestId: '237f4d1a293f99',
publisherNameIdentifier: 'bid.params.publisherNameIdentifier',
height: 600,
width: 300,
},
user: '10462725-da61-4d3a-beff-6d05239e9a6e"',
}],
url: 'http://example.com',
},
};

const serverResponse = {
body: {
'decisions': {
'inline': []
},
'location': {'city': 'Oakville'},
},
};

const result = spec.interpretResponse(serverResponse, bidRequest);
expect(result).to.have.lengthOf(0);
expect(result).to.deep.have.same.members([]);
})

it('should get empty response when bid server returns 204', function() {
expect(spec.interpretResponse({})).to.be.empty;
});
});
});

0 comments on commit e626373

Please sign in to comment.