Skip to content

Commit

Permalink
New bidder & analytics adapter: Concert (attempt #2) (prebid#5623)
Browse files Browse the repository at this point in the history
* Add concert bid adapter, doc and tests.

* Add analytics adapter

* Add email

* fix alert from lgtm

* try to fix test for ie 11

* Handle USP string for PPID

* Fix linking error

* Debug: Find out why IE11 is failing

* More debugging

* More debugging

* Attempt to store queue in-prototype

* Revert "Attempt to store queue in-prototype"

This reverts commit 829ad84.

* More debugging

* More debugging

* Remove Array.includes to support IE11

Co-authored-by: Messay Bekele <[email protected]>
Co-authored-by: Messay Bekele <[email protected]>
Co-authored-by: Andrew Amato <[email protected]>
  • Loading branch information
4 people authored Aug 20, 2020
1 parent 0d8d9bf commit b4c6b47
Show file tree
Hide file tree
Showing 6 changed files with 748 additions and 0 deletions.
120 changes: 120 additions & 0 deletions modules/concertAnalyticsAdapter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import {ajax} from '../src/ajax.js';
import adapter from '../src/AnalyticsAdapter.js';
import CONSTANTS from '../src/constants.json';
import adapterManager from '../src/adapterManager.js';
import * as utils from '../src/utils.js';

const analyticsType = 'endpoint';

// We only want to send about 1% of events for sampling purposes
const SAMPLE_RATE_PERCENTAGE = 1 / 100;
const pageIncludedInSample = sampleAnalytics();

const url = 'https://bids.concert.io/analytics';

const {
EVENTS: {
BID_RESPONSE,
BID_WON,
AUCTION_END
}
} = CONSTANTS;

let queue = [];

let concertAnalytics = Object.assign(adapter({url, analyticsType}), {
track({ eventType, args }) {
switch (eventType) {
case BID_RESPONSE:
if (args.bidder !== 'concert') break;
queue.push(mapBidEvent(eventType, args));
break;

case BID_WON:
if (args.bidder !== 'concert') break;
queue.push(mapBidEvent(eventType, args));
break;

case AUCTION_END:
// Set a delay, as BID_WON events will come after AUCTION_END events
setTimeout(() => sendEvents(), 3000);
break;

default:
break;
}
}
});

function mapBidEvent(eventType, args) {
const { adId, auctionId, cpm, creativeId, width, height, timeToRespond } = args;
const [gamCreativeId, concertRequestId] = getConcertRequestId(creativeId);

const payload = {
event: eventType,
concert_rid: concertRequestId,
adId,
auctionId,
creativeId: gamCreativeId,
position: args.adUnitCode,
url: window.location.href,
cpm,
width,
height,
timeToRespond
}

return payload;
}

/**
* In order to pass back the concert_rid from CBS, it is tucked into the `creativeId`
* slot in the bid response and combined with a pipe `|`. This method splits the creative ID
* and the concert_rid.
*
* @param {string} creativeId
*/
function getConcertRequestId(creativeId) {
if (!creativeId || creativeId.indexOf('|') < 0) return [null, null];

return creativeId.split('|');
}

function sampleAnalytics() {
return Math.random() <= SAMPLE_RATE_PERCENTAGE;
}

function sendEvents() {
concertAnalytics.eventsStorage = queue;

if (!queue.length) return;

if (!pageIncludedInSample) {
utils.logMessage('Page not included in sample for Concert Analytics');
return;
}

try {
const body = JSON.stringify(queue);
ajax(url, () => queue = [], body, {
contentType: 'application/json',
method: 'POST'
});
} catch (err) { utils.logMessage('Concert Analytics error') }
}

// save the base class function
concertAnalytics.originEnableAnalytics = concertAnalytics.enableAnalytics;
concertAnalytics.eventsStorage = [];

// override enableAnalytics so we can get access to the config passed in from the page
concertAnalytics.enableAnalytics = function (config) {
concertAnalytics.originEnableAnalytics(config);
};

adapterManager.registerAnalyticsAdapter({
adapter: concertAnalytics,
code: 'concert'
});

export default concertAnalytics;
11 changes: 11 additions & 0 deletions modules/concertAnalyticsAdapter.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Overview

```
Module Name: Concert Analytics Adapter
Module Type: Analytics Adapter
Maintainer: [email protected]
```

# Description

Analytics adapter for concert.
208 changes: 208 additions & 0 deletions modules/concertBidAdapter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@

import * as utils from '../src/utils.js';
import { registerBidder } from '../src/adapters/bidderFactory.js';
import { getStorageManager } from '../src/storageManager.js'

const BIDDER_CODE = 'concert';
const CONCERT_ENDPOINT = 'https://bids.concert.io';
const USER_SYNC_URL = 'https://cdn.concert.io/lib/bids/sync.html';

export const spec = {
code: BIDDER_CODE,
/**
* 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) {
if (!bid.params.partnerId) {
utils.logWarn('Missing partnerId bid parameter');
return false;
}

return true;
},

/**
* Make a server request from the list of BidRequests.
*
* @param {validBidRequests[]} - an array of bids
* @param {bidderRequest} -
* @return ServerRequest Info describing the request to the server.
*/
buildRequests: function(validBidRequests, bidderRequest) {
utils.logMessage(validBidRequests);
utils.logMessage(bidderRequest);
let payload = {
meta: {
prebidVersion: '$prebid.version$',
pageUrl: bidderRequest.refererInfo.referer,
screen: [window.screen.width, window.screen.height].join('x'),
debug: utils.debugTurnedOn(),
uid: getUid(bidderRequest),
optedOut: hasOptedOutOfPersonalization(),
adapterVersion: '1.1.0',
uspConsent: bidderRequest.uspConsent,
gdprConsent: bidderRequest.gdprConsent
}
}

payload.slots = validBidRequests.map(bidRequest => {
let slot = {
name: bidRequest.adUnitCode,
bidId: bidRequest.bidId,
transactionId: bidRequest.transactionId,
sizes: bidRequest.sizes,
partnerId: bidRequest.params.partnerId,
slotType: bidRequest.params.slotType
}

return slot;
});

utils.logMessage(payload);

return {
method: 'POST',
url: `${CONCERT_ENDPOINT}/bids/prebid`,
data: JSON.stringify(payload)
}
},
/**
* Unpack the response from the server into a list of bids.
*
* @param {ServerResponse} serverResponse A successful response from the server.
* @return {Bid[]} An array of bids which were nested inside the server.
*/
interpretResponse: function(serverResponse, bidRequest) {
utils.logMessage(serverResponse);
utils.logMessage(bidRequest);

const serverBody = serverResponse.body;

if (!serverBody || typeof serverBody !== 'object') {
return [];
}

let bidResponses = [];

bidResponses = serverBody.bids.map(bid => {
return {
requestId: bid.bidId,
cpm: bid.cpm,
width: bid.width,
height: bid.height,
ad: bid.ad,
ttl: bid.ttl,
creativeId: bid.creativeId,
netRevenue: bid.netRevenue,
currency: bid.currency
}
});

if (utils.debugTurnedOn() && serverBody.debug) {
utils.logMessage(`CONCERT`, serverBody.debug);
}

utils.logMessage(bidResponses);
return bidResponses;
},

/**
* 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.
* @param {gdprConsent} object GDPR consent object.
* @param {uspConsent} string US Privacy String.
* @return {UserSync[]} The user syncs which should be dropped.
*/
getUserSyncs: function(syncOptions, serverResponses, gdprConsent, uspConsent) {
const syncs = []
if (syncOptions.iframeEnabled && !hasOptedOutOfPersonalization()) {
let params = [];

if (gdprConsent && (typeof gdprConsent.gdprApplies === 'boolean')) {
params.push(`gdpr_applies=${gdprConsent.gdprApplies ? '1' : '0'}`);
}
if (gdprConsent && (typeof gdprConsent.consentString === 'string')) {
params.push(`gdpr_consent=${gdprConsent.consentString}`);
}
if (uspConsent && (typeof uspConsent === 'string')) {
params.push(`usp_consent=${uspConsent}`);
}

syncs.push({
type: 'iframe',
url: USER_SYNC_URL + (params.length > 0 ? `?${params.join('&')}` : '')
});
}
return syncs;
},

/**
* Register bidder specific code, which will execute if bidder timed out after an auction
* @param {data} Containing timeout specific data
*/
onTimeout: function(data) {
utils.logMessage('concert bidder timed out');
utils.logMessage(data);
},

/**
* Register bidder specific code, which will execute if a bid from this bidder won the auction
* @param {Bid} The bid that won the auction
*/
onBidWon: function(bid) {
utils.logMessage('concert bidder won bid');
utils.logMessage(bid);
}

}

registerBidder(spec);

const storage = getStorageManager();

/**
* Check or generate a UID for the current user.
*/
function getUid(bidderRequest) {
if (hasOptedOutOfPersonalization() || !consentAllowsPpid(bidderRequest)) {
return false;
}

const CONCERT_UID_KEY = 'c_uid';

let uid = storage.getDataFromLocalStorage(CONCERT_UID_KEY);

if (!uid) {
uid = utils.generateUUID();
storage.setDataInLocalStorage(CONCERT_UID_KEY, uid);
}

return uid;
}

/**
* Whether the user has opted out of personalization.
*/
function hasOptedOutOfPersonalization() {
const CONCERT_NO_PERSONALIZATION_KEY = 'c_nap';

return storage.getDataFromLocalStorage(CONCERT_NO_PERSONALIZATION_KEY) === 'true';
}

/**
* Whether the privacy consent strings allow personalization.
*
* @param {BidderRequest} bidderRequest Object which contains any data consent signals
*/
function consentAllowsPpid(bidderRequest) {
/* NOTE: We cannot easily test GDPR consent, without the
* `consent-string` npm module; so will have to rely on that
* happening on the bid-server. */
return !(bidderRequest.uspConsent === 'string' &&
bidderRequest.uspConsent.toUpperCase().substring(0, 2) === '1YY')
}
33 changes: 33 additions & 0 deletions modules/concertBidAdapter.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Overview

```
Module Name: Concert Bid Adapter
Module Type: Bidder Adapter
Maintainer: [email protected]
```

# Description

Module that connects to Concert demand sources

# Test Paramters
```
var adUnits = [
{
code: 'desktop_leaderboard_variable',
mediaTypes: {
banner: {
sizes: [[1030, 590]]
}
}
bids: [
{
bidder: "concert",
params: {
partnerId: 'test_partner'
}
}
]
}
];
```
Loading

0 comments on commit b4c6b47

Please sign in to comment.