-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathanalytics.js
More file actions
333 lines (298 loc) · 10.1 KB
/
analytics.js
File metadata and controls
333 lines (298 loc) · 10.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
/* eslint-disable no-param-reassign */
const STATUS = {
REQUESTED: "REQUESTED",
RECEIVED: "RECEIVED",
NO_BID: "NO_BID",
TIMEOUT: "TIMEOUT",
};
class OptablePrebidAnalytics {
constructor(optableInstance, config = {}) {
if (!optableInstance || typeof optableInstance.witness !== "function") {
throw new Error("OptablePrebidAnalytics requires a valid optable instance with witness() method");
}
this.config = {
debug: config.debug ?? optableInstance.config ?? false,
...config,
};
this.optableInstance = optableInstance;
this.isInitialized = true;
// Store auction data
this.auctions = {};
this.maxAuctionDataSize = 20;
sessionStorage.optableSessionDepth = (Number(sessionStorage?.optableSessionDepth) || 0) + 1;
this.log("OptablePrebidAnalytics initialized");
}
/**
* Log messages if debug is enabled
*/
log(...args) {
if (this.config.debug) {
console.log("[OptablePrebidAnalytics]", ...args); /* eslint-disable-line no-console */
}
}
/**
* Send event to Witness API
*/
async sendToWitnessAPI(eventName, properties = {}) {
if (!this.config.analytics) {
this.log("Witness API calls disabled - would send:", eventName, properties);
return { disabled: true, eventName, properties };
}
try {
await this.optableInstance.witness(eventName, properties);
this.log("Sending to Witness API:", eventName, properties);
} catch (error) {
this.log("Error sending to Witness API:", eventName, properties, error);
throw error;
}
return { disabled: false, eventName, properties };
}
setHooks(pbjs) {
this.log("Processing missed auctionEnd");
pbjs.getEvents().forEach((event) => {
if (event.eventType === "auctionEnd") {
this.log("auction missed");
this.trackAuctionEnd(event.args, true);
}
if (event.eventType === "bidWon") {
this.log("bid won missed");
this.trackBidWon(event.args, true);
}
});
this.log("Hooking into Prebid.js events");
pbjs.onEvent("auctionEnd", (event) => {
this.log("auctionEnd event received");
this.trackAuctionEnd(event);
});
pbjs.onEvent("bidWon", (event) => {
this.log("bidWon event received");
this.trackBidWon(event);
});
}
/**
* Hook into Prebid.js events
*/
hookIntoPrebid(prebidInstance = window.pbjs) {
const pbjs = prebidInstance;
this.prebidInstance = pbjs;
if (typeof pbjs === "undefined") {
this.log("Prebid.js not found");
return false;
}
if (typeof pbjs.onEvent !== "function") {
pbjs.que = pbjs.que || [];
pbjs.que.push(() => this.setHooks(pbjs));
} else {
this.setHooks(pbjs);
}
return true;
}
async trackAuctionEnd(event, missed) {
const { auctionId, timeout, bidderRequests = [], bidsReceived = [], noBids = [], timeoutBids = [] } = event;
window.optable.pageAuctionsCount = (Number(window.optable.pageAuctionsCount) || 0) + 1;
this.log(`Processing auction ${auctionId} with ${bidderRequests.length} bidder requests`);
// Build auction object with bidder requests and EID flags
const auction = {
auctionId,
timeout,
bidderRequests: bidderRequests.map((br) => {
const { bidderCode, bidderRequestId, ortb2, bids = [] } = br;
const domain = ortb2?.site?.domain;
const eids = ortb2?.user?.ext?.eids;
// Optable EIDs
const optableEIDS = eids.filter((e) => e.inserter === "optable.co");
const optableMatchers = [...new Set(optableEIDS.map((e) => e.matcher).filter(Boolean))];
const optableSources = [...new Set(optableEIDS.map((e) => e.source).filter(Boolean))];
// LiveIntent EIDs
const liveintentEIDS = eids
.filter((e) => e.uids?.some((u) => u.ext?.provider === "liveintent.com"))
.map((e) => ({ ...e, uids: e.uids.filter((u) => u.ext?.provider === "liveintent.com") }));
const liSources = [...new Set(liveintentEIDS.map((e) => e.source).filter(Boolean))];
return {
bidderCode,
bidderRequestId,
domain,
hasOEids: optableEIDS.length > 0,
optableMatchers,
optableSources,
hasLiEids: liveintentEIDS.length > 0,
liSources,
status: STATUS.REQUESTED,
bids: bids.map((b) => ({
bidId: b.bidId,
bidderRequestId,
adUnitCode: b.adUnitCode,
adUnitId: b.adUnitId,
transactionId: b.transactionId,
src: b.src,
floorMin: b.floorData?.floorMin,
status: STATUS.REQUESTED,
})),
};
}),
};
// Build lookup tables for 1:many relationship
const requestIndex = {};
const bidIndex = {};
const bidToRequest = {};
auction.bidderRequests.forEach((br) => {
requestIndex[br.bidderRequestId] = br;
br.bids.forEach((bid) => {
bidIndex[bid.bidId] = bid;
bidToRequest[bid.bidId] = br;
});
});
// Merge in bidsReceived → update individual bids as RECEIVED
bidsReceived.forEach((b) => {
const bidId = b.requestId;
const br = bidToRequest[bidId];
if (!br) {
this.log(`No bidderRequest found for bidId=${bidId}`);
return;
}
// Find the specific bid to update
let bidObj = bidIndex[bidId];
if (bidObj) {
// Update existing bid
Object.assign(bidObj, {
status: STATUS.RECEIVED,
cpm: b.cpm,
size: `${b.width}x${b.height}`,
currency: b.currency,
});
} else {
// Create new bid object for this response
bidObj = {
bidId,
bidderRequestId: br.bidderRequestId,
adUnitCode: b.adUnitCode,
adUnitId: b.adUnitId,
transactionId: b.transactionId,
src: b.src,
cpm: b.cpm,
size: `${b.width}x${b.height}`,
currency: b.currency,
status: STATUS.RECEIVED,
};
br.bids.push(bidObj);
bidIndex[bidId] = bidObj;
bidToRequest[bidId] = br;
}
// Update bidder request status to RECEIVED if any bid was received
if (br.status === STATUS.REQUESTED) {
br.status = STATUS.RECEIVED;
}
});
// Handle noBids → mark the entire request as NO_BID
noBids.forEach((nb) => {
const br = requestIndex[nb.bidderRequestId];
if (!br) return;
br.status = STATUS.NO_BID;
// Mark all bids in this request as NO_BID
br.bids.forEach((bid) => {
bid.status = STATUS.NO_BID;
});
});
// Handle timeoutBids → mark the entire request as TIMEOUT
timeoutBids.forEach((tb) => {
const br = requestIndex[tb.bidderRequestId];
if (!br) return;
br.status = STATUS.TIMEOUT;
// Mark all bids in this request as TIMEOUT
br.bids.forEach((bid) => {
bid.status = STATUS.TIMEOUT;
});
});
// Store the processed auction
this.auctions[auctionId] = auction;
// Clean up old auctions
this.cleanupOldAuctions();
// Send to Witness API
try {
const oMatchersSet = new Set();
const oSourcesSet = new Set();
const lSourcesSet = new Set();
let adUnitCode;
let totalBids = 0;
const witnessData = {
bidderRequests: auction.bidderRequests.map((br) => {
br.optableMatchers.forEach((m) => oMatchersSet.add(m));
br.optableSources.forEach((s) => oSourcesSet.add(s));
br.liSources.forEach((s) => lSourcesSet.add(s));
return {
bidderCode: br.bidderCode,
bids: br.bids.map((b) => {
adUnitCode = adUnitCode || b.adUnitCode;
if (b.cpm != null) totalBids += 1;
return {
floorMin: b.floorMin,
cpm: b.cpm,
size: b.size,
bidId: b.bidId,
};
}),
};
}),
auctionId,
adUnitCode,
totalRequests: bidderRequests.length,
totalBids,
optableMatchers: Array.from(oMatchersSet),
optableSources: Array.from(oSourcesSet),
liveintentEIDs: Array.from(lSourcesSet),
missed,
url: `${window.location.hostname}${window.location.pathname}`,
tenant: this.config.tenant,
optableWrapperVersion: SDK_WRAPPER_VERSION, // eslint-disable-line no-undef
prebidjsVersion: this.prebidInstance?.version || "unknown",
sessionDepth: sessionStorage?.optableSessionDepth || 1,
pageAuctionsCount: window.optable?.pageAuctionsCount || 1,
};
// Log summary with bid counts
this.log(
`Auction ${auctionId} processed: ${bidderRequests.length} requests, ${totalBids} total bids, ${bidsReceived.length} received, ${noBids.length} no-bids, ${timeoutBids.length} timeouts`
);
if (window.optable.customAnalytics) {
await window.optable.customAnalytics().then((response) => {
this.log(`Adding custom data to payload ${JSON.stringify(response)}`);
Object.assign(witnessData, response);
});
}
await this.sendToWitnessAPI("auction_processed", {
auction: JSON.stringify(witnessData),
});
} catch (error) {
this.log("Failed to send auction data to Witness:", error);
}
}
trackBidWon(event, missed) {
const filteredEvent = {
auctionId: event.auctionId,
bidderCode: event.bidderCode,
bidId: event.requestId,
tenant: this.config.tenant,
missed,
};
this.log("bidWon filtered event", filteredEvent);
this.sendToWitnessAPI("bid_won", filteredEvent);
}
/**
* Clean up old auctions to prevent memory leaks
*/
cleanupOldAuctions() {
const auctionIds = Object.keys(this.auctions);
if (auctionIds.length > this.maxAuctionDataSize) {
const oldestAuctionId = auctionIds[0];
delete this.auctions[oldestAuctionId];
this.log(`Cleaned up old auction: ${oldestAuctionId}`);
}
}
/**
* Clear all stored data (useful for testing)
*/
clearData() {
this.auctions = {};
this.log("All analytics data cleared");
}
}
export default OptablePrebidAnalytics;