Skip to content

Sonobi analytics #11

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
277 changes: 277 additions & 0 deletions modules/sonobiAnalyticsAdapter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,277 @@
import adapter from '../src/AnalyticsAdapter';
import CONSTANTS from '../src/constants.json';
import adapterManager from '../src/adapterManager';
import {ajaxBuilder} from '../src/ajax';

const utils = require('../src/utils');
let ajax = ajaxBuilder(0);

const DEFAULT_EVENT_URL = 'apex.go.sonobi.com/keymaker';
const analyticsType = 'endpoint';
const QUEUE_TIMEOUT_DEFAULT = 200;
const {
EVENTS: {
AUCTION_INIT,
AUCTION_END,
BID_REQUESTED,
BID_ADJUSTMENT,
BIDDER_DONE,
BID_WON,
BID_RESPONSE,
BID_TIMEOUT
}
} = CONSTANTS;

let initOptions = {};
let auctionCache = {};
let auctionTtl = 60 * 60 * 1000;

function deleteOldAuctions() {
for (let auctionId in auctionCache) {
let auction = auctionCache[auctionId];
if (Date.now() - auction.start > auctionTtl) {
delete auctionCache[auctionId];
}
}
}

function buildAuctionEntity(args) {
return {
'id': args.auctionId,
'start': args.timestamp,
'timeout': args.timeout,
'adUnits': {},
'stats': {},
'queue': [],
'qTimeout': false
};
}
function buildAdUnit(data) {
return `/${initOptions.pubId}/${initOptions.siteId}/${data.adUnitCode.toLowerCase()}`;
}
function getLatency(data) {
if (!data.responseTimestamp) {
return -1;
} else {
return data.responseTimestamp - data.requestTimestamp;
}
}
function getBid(data) {
if (data.cpm) {
return Math.round(data.cpm * 100);
} else {
return 0;
}
}
function buildItem(data, response, phase = 1) {
let size = data.width ? {width: data.width, height: data.height} : {width: data.sizes[0][0], height: data.sizes[0][1]};
return {
'bidid': data.bidId || data.requestId,
'p': phase,
'buyerid': data.bidder.toLowerCase(),
'bid': getBid(data),
'adunit_code': buildAdUnit(data),
's': `${size.width}x${size.height}`,
'latency': getLatency(data),
'response': response,
'jsLatency': getLatency(data),
'buyername': data.bidder.toLowerCase()
};
}
function sendQueue(auctionId) {
let auction = auctionCache[auctionId];
let data = Array.from(auction.queue);
auction.queue = [];
auction.qTimeout = false;
sonobiAdapter.sendData(auction, data);
}
function addToAuctionQueue(auctionId, id) {
let auction = auctionCache[auctionId];
auction.queue = auction.queue.filter((item) => {
if (item.bidid !== id) { return true; }
return auction.stats[id].data.p !== item.p;
});
auction.queue.push(utils.deepClone(auction.stats[id].data));
if (!auction.qTimeout) {
auction.qTimeout = setTimeout(() => {
sendQueue(auctionId);
}, initOptions.delay)
}
}
function updateBidStats(auctionId, id, data) {
let auction = auctionCache[auctionId];
auction.stats[id].data = {...auction.stats[id].data, ...data};
addToAuctionQueue(auctionId, id);
logInfo('Updated Bid Stats: ', auction.stats[id]);
return auction.stats[id];
}

function handleOtherEvents(eventType, args) {
logInfo('Other Event: ' + eventType, args);
}

function handlerAuctionInit(args) {
auctionCache[args.auctionId] = buildAuctionEntity(args);
deleteOldAuctions();
logInfo('Auction Init', args);
}
function handlerBidRequested(args) {
let auction = auctionCache[args.auctionId];
let data = [];
let phase = 1;
let response = 1;
args.bids.forEach(function (bidRequest) {
auction = auctionCache[bidRequest.auctionId]
let built = buildItem(bidRequest, response, phase);
auction.stats[built.bidid] = {id: built.bidid, adUnitCode: bidRequest.adUnitCode, data: built};
addToAuctionQueue(args.auctionId, built.bidid);
})

logInfo('Bids Requested ', data);
}

function handlerBidAdjustment(args) {
logInfo('Bid Adjustment', args);
}
function handlerBidderDone(args) {
logInfo('Bidder Done', args);
}

function handlerAuctionEnd(args) {
let winners = {};
args.bidsReceived.forEach((bid) => {
if (!winners[bid.adUnitCode]) {
winners[bid.adUnitCode] = {bidId: bid.requestId, cpm: bid.cpm};
} else if (winners[bid.adUnitCode].cpm < bid.cpm) {
winners[bid.adUnitCode] = {bidId: bid.requestId, cpm: bid.cpm};
}
})
args.adUnitCodes.forEach((adUnitCode) => {
if (winners[adUnitCode]) {
let bidId = winners[adUnitCode].bidId;
updateBidStats(args.auctionId, bidId, {response: 4});
}
})
logInfo('Auction End', args);
logInfo('Auction Cache', auctionCache[args.auctionId].stats);
}
function handlerBidWon(args) {
let {auctionId, requestId} = args;
let res = updateBidStats(auctionId, requestId, {p: 3, response: 6});
logInfo('Bid Won ', args);
logInfo('Bid Update Result: ', res);
}
function handlerBidResponse(args) {
let {auctionId, requestId, cpm, size, timeToRespond} = args;
updateBidStats(auctionId, requestId, {bid: cpm, s: size, jsLatency: timeToRespond, latency: timeToRespond, p: 2, response: 9});

logInfo('Bid Response ', args);
}
function handlerBidTimeout(args) {
let {auctionId, bidId} = args;
logInfo('Bid Timeout ', args);
updateBidStats(auctionId, bidId, {p: 2, response: 0, latency: args.timeout, jsLatency: args.timeout});
}
let sonobiAdapter = Object.assign(adapter({url: DEFAULT_EVENT_URL, analyticsType}), {
track({eventType, args}) {
switch (eventType) {
case AUCTION_INIT:
handlerAuctionInit(args);
break;
case BID_REQUESTED:
handlerBidRequested(args);
break;
case BID_ADJUSTMENT:
handlerBidAdjustment(args);
break;
case BIDDER_DONE:
handlerBidderDone(args);
break;
case AUCTION_END:
handlerAuctionEnd(args);
break;
case BID_WON:
handlerBidWon(args);
break;
case BID_RESPONSE:
handlerBidResponse(args);
break;
case BID_TIMEOUT:
handlerBidTimeout(args);
break;
default:
handleOtherEvents(eventType, args);
break;
}
},

});

sonobiAdapter.originEnableAnalytics = sonobiAdapter.enableAnalytics;

sonobiAdapter.enableAnalytics = function (config) {
if (this.initConfig(config)) {
logInfo('Analytics adapter enabled', initOptions);
sonobiAdapter.originEnableAnalytics(config);
}
};

sonobiAdapter.initConfig = function (config) {
let isCorrectConfig = true;
initOptions = {};
initOptions.options = utils.deepClone(config.options);

initOptions.pubId = initOptions.options.pubId || null;
initOptions.siteId = initOptions.options.siteId || null;
initOptions.delay = initOptions.options.delay || QUEUE_TIMEOUT_DEFAULT;
if (!initOptions.pubId) {
logError('"options.pubId" is empty');
isCorrectConfig = false;
}
if (!initOptions.siteId) {
logError('"options.siteId" is empty');
isCorrectConfig = false;
}

initOptions.server = DEFAULT_EVENT_URL;
initOptions.host = initOptions.options.host || window.location.hostname;
this.initOptions = initOptions;
return isCorrectConfig;
};

sonobiAdapter.getOptions = function () {
return initOptions;
};

sonobiAdapter.sendData = function (auction, data) {
let url = 'https://' + initOptions.server + '?pageviewid=' + auction.id + '&corscred=1&pubId=' + initOptions.pubId + '&siteId=' + initOptions.siteId;
ajax(
url,
function () { logInfo('Auction [' + auction.id + '] sent ', data); },
JSON.stringify(data),
{
method: 'POST',
// withCredentials: true,
contentType: 'text/plain'
}
);
}

function logInfo(message, meta) {
utils.logInfo(buildLogMessage(message), meta);
}

function logError(message) {
utils.logError(buildLogMessage(message));
}

function buildLogMessage(message) {
return 'Sonobi Prebid Analytics: ' + message;
}

adapterManager.registerAnalyticsAdapter({
adapter: sonobiAdapter,
code: 'sonobi'
});

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

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

# Description

Module that connects to Sonobi's Analytics service

# Test Parameters
```

pbjs.enableAnalytics({
provider: 'sonobi',
options: {
pubId: 'ffBB352',
siteId: 57463,
delay: 300
}
});
```
89 changes: 89 additions & 0 deletions test/spec/modules/sonobiAnalyticsAdapter_spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import sonobiAnalytics from 'modules/sonobiAnalyticsAdapter';
import {expect} from 'chai';
let events = require('src/events');
let adapterManager = require('src/adapterManager').default;
let constants = require('src/constants.json');

describe('Sonobi Prebid Analytic', function () {
let xhr;
let requests = [];
var clock;

describe('enableAnalytics', function () {
beforeEach(function () {
requests = [];
xhr = sinon.useFakeXMLHttpRequest();
xhr.onCreate = request => requests.push(request);
sinon.stub(events, 'getEvents').returns([]);
clock = sinon.useFakeTimers(Date.now());
});

afterEach(function () {
xhr.restore();
events.getEvents.restore();
clock.restore();
});

after(function () {
sonobiAnalytics.disableAnalytics();
});

it('should catch all events', function (done) {
const initOptions = {
pubId: 'A3B254F',
siteId: '1234',
delay: 100
};

sonobiAnalytics.enableAnalytics(initOptions)

const bid = {
bidderCode: 'sonobi_test_bid',
width: 300,
height: 250,
statusMessage: 'Bid available',
adId: '1234',
auctionId: '13',
responseTimestamp: 1496410856397,
requestTimestamp: 1496410856295,
cpm: 1.13,
bidder: 'sonobi',
adUnitCode: 'dom-sample-id',
timeToRespond: 100,
placementCode: 'placementtest'
};

// Step 1: Initialize adapter
adapterManager.enableAnalytics({
provider: 'sonobi',
options: initOptions
});

// Step 2: Send init auction event
events.emit(constants.EVENTS.AUCTION_INIT, {config: initOptions, auctionId: '13', timestamp: Date.now()});

expect(sonobiAnalytics.initOptions).to.have.property('pubId', 'A3B254F');
expect(sonobiAnalytics.initOptions).to.have.property('siteId', '1234');
expect(sonobiAnalytics.initOptions).to.have.property('delay', 100);
// Step 3: Send bid requested event
events.emit(constants.EVENTS.BID_REQUESTED, { bids: [bid], auctionId: '13' });

// Step 4: Send bid response event
events.emit(constants.EVENTS.BID_RESPONSE, bid);

// Step 5: Send bid won event
events.emit(constants.EVENTS.BID_WON, bid);

// Step 6: Send bid timeout event
events.emit(constants.EVENTS.BID_TIMEOUT, {auctionId: '13'});

// Step 7: Send auction end event
events.emit(constants.EVENTS.AUCTION_END, {auctionId: '13', bidsReceived: [bid]});

clock.tick(5000);
expect(requests).to.have.length(1);
expect(JSON.parse(requests[0].requestBody)).to.have.length(3)
done();
});
});
});