Skip to content

Commit e626373

Browse files
Flipp Bid Adapter : initial release (prebid#10412)
* 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 <[email protected]> * 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 <[email protected]>
1 parent 5c9e0cf commit e626373

File tree

3 files changed

+397
-0
lines changed

3 files changed

+397
-0
lines changed

modules/flippBidAdapter.js

+183
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
import {isEmpty, parseUrl} from '../src/utils.js';
2+
import { registerBidder } from '../src/adapters/bidderFactory.js';
3+
import { BANNER } from '../src/mediaTypes.js';
4+
import {getStorageManager} from '../src/storageManager.js';
5+
6+
const NETWORK_ID = 11090;
7+
const AD_TYPES = [4309, 641];
8+
const DTX_TYPES = [5061];
9+
const TARGET_NAME = 'inline';
10+
const BIDDER_CODE = 'flipp';
11+
const ENDPOINT = 'https://gateflipp.flippback.com/flyer-locator-service/client_bidding';
12+
const DEFAULT_TTL = 30;
13+
const DEFAULT_CURRENCY = 'USD';
14+
const DEFAULT_CREATIVE_TYPE = 'NativeX';
15+
const VALID_CREATIVE_TYPES = ['DTX', 'NativeX'];
16+
const FLIPP_USER_KEY = 'flipp-uid';
17+
const COMPACT_DEFAULT_HEIGHT = 600;
18+
19+
let userKey = null;
20+
export const storage = getStorageManager({bidderCode: BIDDER_CODE});
21+
22+
export function getUserKey(options = {}) {
23+
if (userKey) {
24+
return userKey;
25+
}
26+
27+
// If the partner provides the user key use it, otherwise fallback to cookies
28+
if (options.userKey && isValidUserKey(options.userKey)) {
29+
userKey = options.userKey;
30+
return options.userKey;
31+
}
32+
// Grab from Cookie
33+
const foundUserKey = storage.cookiesAreEnabled() && storage.getCookie(FLIPP_USER_KEY);
34+
if (foundUserKey) {
35+
return foundUserKey;
36+
}
37+
38+
// Generate if none found
39+
userKey = generateUUID();
40+
41+
// Set cookie
42+
if (storage.cookiesAreEnabled()) {
43+
storage.setCookie(FLIPP_USER_KEY, userKey);
44+
}
45+
46+
return userKey;
47+
}
48+
49+
function isValidUserKey(userKey) {
50+
return !userKey.startsWith('#');
51+
}
52+
53+
const generateUUID = () => {
54+
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => {
55+
const r = (Math.random() * 16) | 0;
56+
const v = c === 'x' ? r : (r & 0x3) | 0x8;
57+
return v.toString(16);
58+
});
59+
};
60+
61+
/**
62+
* Determines if a creativeType is valid
63+
*
64+
* @param {string} creativeType The Creative Type to validate.
65+
* @return string creativeType if this is a valid Creative Type, and 'NativeX' otherwise.
66+
*/
67+
const validateCreativeType = (creativeType) => {
68+
if (creativeType && VALID_CREATIVE_TYPES.includes(creativeType)) {
69+
return creativeType;
70+
} else {
71+
return DEFAULT_CREATIVE_TYPE;
72+
}
73+
};
74+
75+
const getAdTypes = (creativeType) => {
76+
if (creativeType === 'DTX') {
77+
return DTX_TYPES;
78+
}
79+
return AD_TYPES;
80+
}
81+
82+
export const spec = {
83+
code: BIDDER_CODE,
84+
supportedMediaTypes: [BANNER],
85+
/**
86+
* Determines whether or not the given bid request is valid.
87+
*
88+
* @param {BidRequest} bid The bid params to validate.
89+
* @return boolean True if this is a valid bid, and false otherwise.
90+
*/
91+
isBidRequestValid: function(bid) {
92+
return !!(bid.params.siteId) && !!(bid.params.publisherNameIdentifier);
93+
},
94+
/**
95+
* Make a server request from the list of BidRequests.
96+
*
97+
* @param {BidRequest[]} validBidRequests[] an array of bids
98+
* @param {BidderRequest} bidderRequest master bidRequest object
99+
* @return ServerRequest Info describing the request to the server.
100+
*/
101+
buildRequests: function(validBidRequests, bidderRequest) {
102+
const urlParams = parseUrl(bidderRequest.refererInfo.page).search;
103+
const contentCode = urlParams['flipp-content-code'];
104+
const userKey = getUserKey(validBidRequests[0]?.params);
105+
const placements = validBidRequests.map((bid, index) => {
106+
const options = bid.params.options || {};
107+
if (!options.hasOwnProperty('startCompact')) {
108+
options.startCompact = true;
109+
}
110+
return {
111+
divName: TARGET_NAME,
112+
networkId: NETWORK_ID,
113+
siteId: bid.params.siteId,
114+
adTypes: getAdTypes(bid.params.creativeType),
115+
count: 1,
116+
...(!isEmpty(bid.params.zoneIds) && {zoneIds: bid.params.zoneIds}),
117+
properties: {
118+
...(!isEmpty(contentCode) && {contentCode: contentCode.slice(0, 32)}),
119+
},
120+
options,
121+
prebid: {
122+
requestId: bid.bidId,
123+
publisherNameIdentifier: bid.params.publisherNameIdentifier,
124+
height: bid.mediaTypes.banner.sizes[index][0],
125+
width: bid.mediaTypes.banner.sizes[index][1],
126+
creativeType: validateCreativeType(bid.params.creativeType),
127+
}
128+
}
129+
});
130+
return {
131+
method: 'POST',
132+
url: ENDPOINT,
133+
data: {
134+
placements,
135+
url: bidderRequest.refererInfo.page,
136+
user: {
137+
key: userKey,
138+
},
139+
},
140+
}
141+
},
142+
/**
143+
* Unpack the response from the server into a list of bids.
144+
*
145+
* @param {ServerResponse} serverResponse A successful response from the server.
146+
* @param {BidRequest} bidRequest A bid request object
147+
* @return {Bid[]} An array of bids which were nested inside the server.
148+
*/
149+
interpretResponse: function(serverResponse, bidRequest) {
150+
if (!serverResponse?.body) return [];
151+
const placements = bidRequest.data.placements;
152+
const res = serverResponse.body;
153+
if (!isEmpty(res) && !isEmpty(res.decisions) && !isEmpty(res.decisions.inline)) {
154+
return res.decisions.inline.map(decision => {
155+
const placement = placements.find(p => p.prebid.requestId === decision.prebid?.requestId);
156+
const height = placement.options?.startCompact ? COMPACT_DEFAULT_HEIGHT : decision.height;
157+
return {
158+
bidderCode: BIDDER_CODE,
159+
requestId: decision.prebid?.requestId,
160+
cpm: decision.prebid?.cpm,
161+
width: decision.width,
162+
height,
163+
creativeId: decision.adId,
164+
currency: DEFAULT_CURRENCY,
165+
netRevenue: true,
166+
ttl: DEFAULT_TTL,
167+
ad: decision.prebid?.creative,
168+
}
169+
});
170+
}
171+
return [];
172+
},
173+
174+
/**
175+
* Register the user sync pixels which should be dropped after the auction.
176+
*
177+
* @param {SyncOptions} syncOptions Which user syncs are allowed?
178+
* @param {ServerResponse[]} serverResponses List of server's responses.
179+
* @return {UserSync[]} The user syncs which should be dropped.
180+
*/
181+
getUserSyncs: (syncOptions, serverResponses) => [],
182+
}
183+
registerBidder(spec);

modules/flippBidAdapter.md

+44
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
# Overview
2+
3+
```
4+
Module Name: Flipp Bid Adapter
5+
Module Type: Bidder Adapter
6+
Maintainer: [email protected]
7+
```
8+
9+
# Description
10+
11+
This module connects publishers to Flipp's Shopper Experience via Prebid.js.
12+
13+
14+
# Test parameters
15+
16+
```javascript
17+
var adUnits = [
18+
{
19+
code: 'flipp-scroll-ad-content',
20+
mediaTypes: {
21+
banner: {
22+
sizes: [
23+
[300, 600]
24+
]
25+
}
26+
},
27+
bids: [
28+
{
29+
bidder: 'flipp',
30+
params: {
31+
creativeType: 'NativeX', // Optional, can be one of 'NativeX' (default) or 'DTX'
32+
publisherNameIdentifier: 'wishabi-test-publisher', // Required
33+
siteId: 1192075, // Required
34+
zoneIds: [260678], // Optional
35+
userKey: "", // Optional
36+
options: {
37+
startCompact: true // Optional, default to true
38+
}
39+
}
40+
}
41+
]
42+
}
43+
]
44+
```
+170
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
import {expect} from 'chai';
2+
import {spec} from 'modules/flippBidAdapter';
3+
import {newBidder} from 'src/adapters/bidderFactory';
4+
const ENDPOINT = 'https://gateflipp.flippback.com/flyer-locator-service/client_bidding';
5+
describe('flippAdapter', function () {
6+
const adapter = newBidder(spec);
7+
8+
describe('inherited functions', function () {
9+
it('exists and is a function', function () {
10+
expect(adapter.callBids).to.exist.and.to.be.a('function');
11+
});
12+
});
13+
14+
describe('isBidRequestValid', function () {
15+
const bid = {
16+
bidder: 'flipp',
17+
params: {
18+
publisherNameIdentifier: 'random',
19+
siteId: 1234,
20+
zoneIds: [1, 2, 3, 4],
21+
}
22+
};
23+
it('should return true when required params found', function () {
24+
expect(spec.isBidRequestValid(bid)).to.equal(true);
25+
});
26+
27+
it('should return false when required params are not passed', function () {
28+
let invalidBid = Object.assign({}, bid);
29+
invalidBid.params = { siteId: 1234 }
30+
expect(spec.isBidRequestValid(invalidBid)).to.equal(false);
31+
});
32+
});
33+
34+
describe('buildRequests', function () {
35+
const bidRequests = [{
36+
bidder: 'flipp',
37+
params: {
38+
siteId: 1234,
39+
},
40+
adUnitCode: '/10000/unit_code',
41+
sizes: [[300, 600]],
42+
mediaTypes: {banner: {sizes: [[300, 600]]}},
43+
bidId: '237f4d1a293f99',
44+
bidderRequestId: '1a857fa34c1c96',
45+
auctionId: 'a297d1aa-7900-4ce4-a0aa-caa8d46c4af7',
46+
transactionId: '00b2896c-2731-4f01-83e4-7a3ad5da13b6',
47+
}];
48+
const bidderRequest = {
49+
refererInfo: {
50+
referer: 'http://example.com'
51+
}
52+
};
53+
54+
it('sends bid request to ENDPOINT via POST', function () {
55+
const request = spec.buildRequests(bidRequests, bidderRequest);
56+
expect(request.method).to.equal('POST');
57+
});
58+
59+
it('sends bid request to ENDPOINT with query parameter', function () {
60+
const request = spec.buildRequests(bidRequests, bidderRequest);
61+
expect(request.url).to.equal(ENDPOINT);
62+
});
63+
});
64+
65+
describe('interpretResponse', function() {
66+
it('should get correct bid response', function() {
67+
const bidRequest = {
68+
method: 'POST',
69+
url: ENDPOINT,
70+
data: {
71+
placements: [{
72+
divName: 'slot',
73+
networkId: 12345,
74+
siteId: 12345,
75+
adTypes: [12345],
76+
count: 1,
77+
prebid: {
78+
requestId: '237f4d1a293f99',
79+
publisherNameIdentifier: 'bid.params.publisherNameIdentifier',
80+
height: 600,
81+
width: 300,
82+
},
83+
user: '10462725-da61-4d3a-beff-6d05239e9a6e"',
84+
}],
85+
url: 'http://example.com',
86+
},
87+
};
88+
89+
const serverResponse = {
90+
body: {
91+
'decisions': {
92+
'inline': [{
93+
'bidCpm': 1,
94+
'adId': 262838368,
95+
'height': 600,
96+
'width': 300,
97+
'storefront': { 'flyer_id': 5435567 },
98+
'prebid': {
99+
'requestId': '237f4d1a293f99',
100+
'cpm': 1.11,
101+
'creative': 'Returned from server',
102+
}
103+
}]
104+
},
105+
'location': {'city': 'Oakville'},
106+
},
107+
};
108+
109+
const expectedResponse = [
110+
{
111+
bidderCode: 'flipp',
112+
requestId: '237f4d1a293f99',
113+
currency: 'USD',
114+
cpm: 1.11,
115+
netRevenue: true,
116+
width: 300,
117+
height: 600,
118+
creativeId: 262838368,
119+
ttl: 30,
120+
ad: 'Returned from server',
121+
}
122+
];
123+
124+
const result = spec.interpretResponse(serverResponse, bidRequest);
125+
expect(result).to.have.lengthOf(1);
126+
expect(result).to.deep.have.same.members(expectedResponse);
127+
});
128+
129+
it('should get empty bid response when no ad is returned', function() {
130+
const bidRequest = {
131+
method: 'POST',
132+
url: ENDPOINT,
133+
data: {
134+
placements: [{
135+
divName: 'slot',
136+
networkId: 12345,
137+
siteId: 12345,
138+
adTypes: [12345],
139+
count: 1,
140+
prebid: {
141+
requestId: '237f4d1a293f99',
142+
publisherNameIdentifier: 'bid.params.publisherNameIdentifier',
143+
height: 600,
144+
width: 300,
145+
},
146+
user: '10462725-da61-4d3a-beff-6d05239e9a6e"',
147+
}],
148+
url: 'http://example.com',
149+
},
150+
};
151+
152+
const serverResponse = {
153+
body: {
154+
'decisions': {
155+
'inline': []
156+
},
157+
'location': {'city': 'Oakville'},
158+
},
159+
};
160+
161+
const result = spec.interpretResponse(serverResponse, bidRequest);
162+
expect(result).to.have.lengthOf(0);
163+
expect(result).to.deep.have.same.members([]);
164+
})
165+
166+
it('should get empty response when bid server returns 204', function() {
167+
expect(spec.interpretResponse({})).to.be.empty;
168+
});
169+
});
170+
});

0 commit comments

Comments
 (0)