Skip to content

Commit 95df399

Browse files
authored
AdPlus Analytics Adapter : initial release (#13493)
* New Analytics Adapter: AdPlus Analytics Adapter * Formatted md * Reformatted md
1 parent af15395 commit 95df399

File tree

3 files changed

+353
-0
lines changed

3 files changed

+353
-0
lines changed

modules/adplusAnalyticsAdapter.js

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
import adapter from '../libraries/analyticsAdapter/AnalyticsAdapter.js';
2+
import adapterManager from '../src/adapterManager.js';
3+
import { logInfo, logError } from '../src/utils.js';
4+
import { EVENTS } from '../src/constants.js';
5+
import { ajax } from '../src/ajax.js';
6+
7+
const { AUCTION_END, BID_WON } = EVENTS;
8+
const ANALYTICS_CODE = 'adplus';
9+
const SERVER_URL = 'https://ssp-dev.ad-plus.com.tr/server/analytics/bids';
10+
const SEND_DELAY_MS = 200;
11+
const MAX_RETRIES = 3;
12+
13+
let auctionBids = {};
14+
let sendQueue = [];
15+
let isSending = false;
16+
17+
const adplusAnalyticsAdapter = Object.assign(adapter({ SERVER_URL, analyticsType: 'endpoint' }), {
18+
track({ eventType, args }) {
19+
try {
20+
switch (eventType) {
21+
case AUCTION_END:
22+
auctionBids[args.auctionId] = auctionBids[args.auctionId] || {};
23+
(args.bidsReceived || []).forEach(bid => {
24+
const adUnit = bid.adUnitCode;
25+
auctionBids[args.auctionId][adUnit] = auctionBids[args.auctionId][adUnit] || [];
26+
auctionBids[args.auctionId][adUnit].push({
27+
type: 'bid',
28+
bidder: bid.bidderCode,
29+
auctionId: bid.auctionId,
30+
adUnitCode: bid.adUnitCode,
31+
cpm: bid.cpm,
32+
currency: bid.currency,
33+
size: bid.size,
34+
width: bid.width,
35+
height: bid.height,
36+
creativeId: bid.creativeId,
37+
timeToRespond: bid.timeToRespond,
38+
netRevenue: bid.netRevenue,
39+
dealId: bid.dealId || null,
40+
});
41+
});
42+
break;
43+
44+
case BID_WON:
45+
const bid = args;
46+
const adUnitBids = (auctionBids[bid.auctionId] || {})[bid.adUnitCode];
47+
if (!adUnitBids) {
48+
logInfo(`[adplusAnalyticsAdapter] No bid data for auction ${bid.auctionId}, ad unit ${bid.adUnitCode}`);
49+
return;
50+
}
51+
52+
const winningBidData = {
53+
type: BID_WON,
54+
bidder: bid.bidderCode,
55+
auctionId: bid.auctionId,
56+
adUnitCode: bid.adUnitCode,
57+
cpm: bid.cpm,
58+
currency: bid.currency,
59+
size: bid.size,
60+
width: bid.width,
61+
height: bid.height,
62+
creativeId: bid.creativeId,
63+
timeToRespond: bid.timeToRespond,
64+
netRevenue: bid.netRevenue,
65+
dealId: bid.dealId || null,
66+
};
67+
68+
const payload = {
69+
auctionId: bid.auctionId,
70+
adUnitCode: bid.adUnitCode,
71+
winningBid: winningBidData,
72+
allBids: adUnitBids
73+
};
74+
75+
sendQueue.push(payload);
76+
if (!isSending) {
77+
processQueue();
78+
}
79+
break;
80+
81+
default:
82+
break;
83+
}
84+
} catch (err) {
85+
logError(`[adplusAnalyticsAdapter] Error processing event ${eventType}`, err);
86+
}
87+
}
88+
});
89+
90+
function processQueue() {
91+
if (sendQueue.length === 0) {
92+
isSending = false;
93+
return;
94+
}
95+
96+
isSending = true;
97+
const nextPayload = sendQueue.shift();
98+
sendWithRetries(nextPayload, 0);
99+
}
100+
101+
function sendWithRetries(payload, attempt) {
102+
const payloadStr = JSON.stringify(payload);
103+
104+
ajax(
105+
SERVER_URL,
106+
{
107+
success: () => {
108+
logInfo(`[adplusAnalyticsAdapter] Sent BID_WON payload (attempt ${attempt + 1})`);
109+
setTimeout(() => {
110+
processQueue();
111+
}, SEND_DELAY_MS);
112+
},
113+
error: () => {
114+
if (attempt < MAX_RETRIES - 1) {
115+
logError(`[adplusAnalyticsAdapter] Send failed (attempt ${attempt + 1}), retrying...`);
116+
setTimeout(() => {
117+
sendWithRetries(payload, attempt + 1);
118+
}, SEND_DELAY_MS);
119+
} else {
120+
logError(`[adplusAnalyticsAdapter] Failed to send after ${MAX_RETRIES} attempts`);
121+
setTimeout(() => {
122+
processQueue();
123+
}, SEND_DELAY_MS);
124+
}
125+
}
126+
},
127+
payloadStr,
128+
{
129+
method: 'POST',
130+
contentType: 'application/json',
131+
},
132+
);
133+
}
134+
135+
adplusAnalyticsAdapter.originEnableAnalytics = adplusAnalyticsAdapter.enableAnalytics;
136+
137+
adplusAnalyticsAdapter.enableAnalytics = function (config) {
138+
adplusAnalyticsAdapter.originEnableAnalytics(config);
139+
logInfo('[adplusAnalyticsAdapter] Analytics enabled with config:', config);
140+
};
141+
142+
adapterManager.registerAnalyticsAdapter({
143+
adapter: adplusAnalyticsAdapter,
144+
code: ANALYTICS_CODE
145+
});
146+
147+
adplusAnalyticsAdapter.auctionBids = auctionBids;
148+
149+
adplusAnalyticsAdapter.reset = function () {
150+
auctionBids = {};
151+
adplusAnalyticsAdapter.auctionBids = auctionBids;
152+
};
153+
154+
export default adplusAnalyticsAdapter;

modules/adplusAnalyticsAdapter.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# Overview
2+
3+
Module Name: AdPlus Analytics Adapter
4+
5+
Module Type: Analytics Adapter
6+
7+
Maintainer: [email protected]
8+
9+
---
10+
11+
# Description
12+
13+
Analytics adapter for AdPlus platform. Contact [[email protected]]() if you have any questions about integration.
14+
15+
---
16+
17+
# Example Configuration
18+
19+
```javascript
20+
pbjs.enableAnalytics({
21+
provider: 'adplus',
22+
});
23+
```
Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
import adplusAnalyticsAdapter from 'modules/adplusAnalyticsAdapter.js';
2+
import { expect } from 'chai';
3+
import adapterManager from 'src/adapterManager.js';
4+
import { server } from 'test/mocks/xhr.js';
5+
import { EVENTS } from 'src/constants.js';
6+
import sinon from 'sinon';
7+
8+
let events = require('src/events');
9+
10+
describe('AdPlus analytics adapter', function () {
11+
let sandbox, clock;
12+
13+
beforeEach(function () {
14+
sandbox = sinon.createSandbox();
15+
sandbox.spy(console, 'log');
16+
17+
clock = sandbox.useFakeTimers();
18+
sandbox.stub(events, 'getEvents').returns([]);
19+
adapterManager.enableAnalytics({ provider: 'adplus' });
20+
});
21+
22+
afterEach(function () {
23+
sandbox.restore();
24+
adplusAnalyticsAdapter.reset();
25+
});
26+
27+
const auctionId = 'test-auction-123';
28+
29+
const bidsReceived = [
30+
{
31+
bidderCode: 'adplus',
32+
auctionId,
33+
adUnitCode: 'adunit-1',
34+
cpm: 5,
35+
currency: 'USD',
36+
size: '300x250',
37+
width: 300,
38+
height: 250,
39+
creativeId: 'crea-1',
40+
timeToRespond: 120,
41+
netRevenue: true,
42+
dealId: null
43+
},
44+
{
45+
bidderCode: 'adplus',
46+
auctionId,
47+
adUnitCode: 'adunit-2',
48+
cpm: 7,
49+
currency: 'USD',
50+
size: '728x90',
51+
width: 728,
52+
height: 90,
53+
creativeId: 'crea-2',
54+
timeToRespond: 110,
55+
netRevenue: true,
56+
dealId: 'deal123'
57+
}
58+
];
59+
60+
const bidWon1 = {
61+
auctionId,
62+
adUnitCode: 'adunit-1',
63+
bidderCode: 'adplus',
64+
cpm: 5,
65+
currency: 'USD',
66+
size: '300x250',
67+
width: 300,
68+
height: 250,
69+
creativeId: 'crea-1',
70+
timeToRespond: 120,
71+
netRevenue: true,
72+
dealId: null
73+
};
74+
75+
const bidWon2 = {
76+
auctionId,
77+
adUnitCode: 'adunit-2',
78+
bidderCode: 'adplus',
79+
cpm: 7,
80+
currency: 'USD',
81+
size: '728x90',
82+
width: 728,
83+
height: 90,
84+
creativeId: 'crea-2',
85+
timeToRespond: 110,
86+
netRevenue: true,
87+
dealId: 'deal123'
88+
};
89+
90+
it('should store bids on AUCTION_END and not send immediately', function () {
91+
events.emit(EVENTS.AUCTION_END, {
92+
auctionId,
93+
bidsReceived
94+
});
95+
96+
expect(server.requests.length).to.equal(0);
97+
98+
const storedData = adplusAnalyticsAdapter.auctionBids[auctionId];
99+
expect(storedData).to.exist;
100+
expect(Object.keys(storedData)).to.have.length(2);
101+
expect(storedData['adunit-1'][0]).to.include({
102+
auctionId,
103+
adUnitCode: 'adunit-1',
104+
bidder: 'adplus',
105+
cpm: 5,
106+
currency: 'USD'
107+
});
108+
});
109+
110+
it('should batch BID_WON events and send after delay with retries', function (done) {
111+
// First, send AUCTION_END to prepare data
112+
events.emit(EVENTS.AUCTION_END, { auctionId, bidsReceived });
113+
114+
// Emit first BID_WON - should send immediately
115+
events.emit(EVENTS.BID_WON, bidWon1);
116+
117+
clock.tick(0);
118+
expect(server.requests.length).to.equal(1);
119+
120+
// Fail first request, triggers retry after 200ms
121+
server.requests[0].respond(500, {}, 'Internal Server Error');
122+
clock.tick(200);
123+
124+
expect(server.requests.length).to.equal(2);
125+
126+
// Fail second (retry) request, triggers next retry
127+
server.requests[1].respond(500, {}, 'Internal Server Error');
128+
clock.tick(200);
129+
130+
expect(server.requests.length).to.equal(3);
131+
132+
// Succeed on third retry
133+
server.requests[2].respond(200, { 'Content-Type': 'application/json' }, JSON.stringify({ status: 'ok' }));
134+
135+
// Now emit second BID_WON - queue is empty, should send immediately
136+
events.emit(EVENTS.BID_WON, bidWon2);
137+
138+
// Should wait 200ms after emit.
139+
expect(server.requests.length).to.equal(3);
140+
141+
// Sends the second BID_WON data after 200ms
142+
clock.tick(200);
143+
expect(server.requests.length).to.equal(4);
144+
145+
// Succeed second BID_WON send
146+
server.requests[3].respond(200, { 'Content-Type': 'application/json' }, JSON.stringify({ status: 'ok' }));
147+
148+
// Validate payloads
149+
const payload1 = JSON.parse(server.requests[0].requestBody);
150+
const payload2 = JSON.parse(server.requests[3].requestBody);
151+
152+
expect(payload1.winningBid).to.include({
153+
auctionId,
154+
adUnitCode: 'adunit-1',
155+
bidder: 'adplus',
156+
cpm: 5,
157+
});
158+
159+
expect(payload2.winningBid).to.include({
160+
auctionId,
161+
adUnitCode: 'adunit-2',
162+
bidder: 'adplus',
163+
cpm: 7,
164+
});
165+
166+
done();
167+
});
168+
169+
it('should skip BID_WON if no auction data available', function () {
170+
// Emit BID_WON without AUCTION_END first
171+
expect(() => events.emit(EVENTS.BID_WON, bidWon1)).to.not.throw();
172+
173+
// No ajax call since no auctionData
174+
expect(server.requests.length).to.equal(0);
175+
});
176+
});

0 commit comments

Comments
 (0)