PageSourceSearch

https://solitairex.io/bidder.js?v=46

js solitairex.io collected 2026-09-24 17:50:48 UTC 28,108 bytes, 659 lines download raw bytes

1(function () {
2    'use strict';
3
4    var STORAGE_KEY = 'pm_bid_stats';
5    var USER_ID_KEY = 'pm_user_id';
6    var STORAGE_VERSION = 4;
7    var LEARNING_THRESHOLD = 10;
8    var TIER1_MAX = 10;
9    var DATA_TTL_DAYS = 7;
10    var MAX_RECENT_CPMS = 30;
11    var FLOOR_MAX = 2.00;
12    var EMPTY_STREAK_TIER2 = 4;
13    var EXPLORE_EVERY_N = 5;
14
15    // ── Anti-gaming: jitter range for floor adjustments ──
16    // Each win/loss applies a random bump in [JITTER_MIN, JITTER_MAX] so DSPs
17    // cannot reverse-engineer a fixed formula.
18    var JITTER_MIN = 0.005;   // 0.5%
19    var JITTER_MAX = 0.015;   // 1.5%
20
21    // ── Anti-gaming: batch floor updates every 12–24 hours ──
22    // Floors do not change immediately after an auction; they update once the
23    // batch window expires.  The window is randomized to break the cause-and-
24    // effect link between individual auctions and floor moves.
25    var BATCH_INTERVAL_MIN = 12 * 3600 * 1000;  // 12 h in ms
26    var BATCH_INTERVAL_MAX = 24 * 3600 * 1000;  // 24 h in ms
27
28    // ── Anti-gaming: cap floor at 80–85 % of average eCPM ──
29    // Never squeeze the floor all the way to 100 % so DSPs still feel a margin.
30    var FLOOR_CAP_MIN = 0.70;
31    var FLOOR_CAP_MAX = 0.75;
32
33    // ── Control group: 10 % of users always receive a static country-minimum floor ──
34    // Stored permanently in localStorage so the same user stays in the same group.
35    var CONTROL_GROUP_THRESHOLD = 0.10;
36
37    // Country-based hard minimum floors (EUR CPM)
38    // Set window.userCountry = 'US' / 'CA' / 'GB' etc. server-side before this script loads
39    var TIER1_COUNTRIES = ['GB', 'AU', 'DE', 'FR', 'NL', 'SE', 'NO', 'DK', 'FI', 'CH', 'AT', 'IE', 'NZ', 'JP', 'SG'];
40
41    function getCountryFloor() {
42        var country = (window.userCountry || '').toUpperCase();
43        if (country === 'US') return 0.20;
44        if (country === 'CA') return 0.15;
45        if (TIER1_COUNTRIES.indexOf(country) >= 0) return 0.10;
46        return 0.04;
47    }
48
49    var allBidderDefs = [];
50    var adSlots = [];
51    var auctionCount = 0;
52    var lastAuctionContext = null;
53
54    function getUserId() {
55        try {
56            var id = localStorage.getItem(USER_ID_KEY);
57            if (!id) {
58                id = 'u_' + Date.now().toString(36) + '_' + Math.random().toString(36).slice(2, 10);
59                localStorage.setItem(USER_ID_KEY, id);
60            }
61            return id;
62        } catch (e) { return 'unknown'; }
63    }
64
65    var LOGGING_ENABLED = false;
66
67    function logAuction(data) {
68        if (!LOGGING_ENABLED) return;
69        try {
70            fetch('https://auction-logger.solitairen-contacts.workers.dev/log?key=Lala33*', {
71                method: 'POST',
72                headers: { 'Content-Type': 'application/json' },
73                body: JSON.stringify(data)
74            });
75        } catch (e) { }
76    }
77
78    function freshStats() {
79        return {
80            version: STORAGE_VERSION,
81            bidders: {},
82            adUnits: {},
83            totalWins: 0,
84            // Assigned once per browser; value in [0, 1).
85            // Users below CONTROL_GROUP_THRESHOLD get a static floor forever.
86            userGroup: Math.random(),
87            updatedAt: Date.now()
88        };
89    }
90
91    // Always returns a valid stats object, never null
92    function load() {
93        try {
94            var raw = localStorage.getItem(STORAGE_KEY);
95            if (raw) {
96                var parsed = JSON.parse(raw);
97                if (parsed && parsed.version === STORAGE_VERSION && typeof parsed.totalWins === 'number') {
98                    // Back-fill userGroup for sessions created before this version
99                    if (typeof parsed.userGroup !== 'number') {
100                        parsed.userGroup = Math.random();
101                    }
102                    pruneStaleData(parsed);
103                    return parsed;
104                }
105            }
106        } catch (e) { }
107        return freshStats();
108    }
109
110    // THE stats object — guaranteed non-null
111    var stats = freshStats();
112    try { stats = load(); } catch (e) { }
113
114    function getStats() {
115        if (!stats) stats = freshStats();
116        return stats;
117    }
118
119    function save() {
120        try {
121            var s = getStats();
122            s.updatedAt = Date.now();
123            localStorage.setItem(STORAGE_KEY, JSON.stringify(s));
124        } catch (e) { }
125    }
126
127    function pruneStaleData(s) {
128        var cutoff = Date.now() - DATA_TTL_DAYS * 86400000;
129        Object.keys(s.bidders).forEach(function (b) {
130            if (s.bidders[b].lastSeen < cutoff) delete s.bidders[b];
131        });
132    }
133
134    function scoreBidder(b) {
135        var winRate = b.wins / Math.max(b.bids, 1);
136        var avgCpm = b.wins > 0 ? b.totalCpm / b.wins : 0;
137        return winRate * avgCpm;
138    }
139
140    function classifyBidders() {
141        var s = getStats();
142        var scored = Object.entries(s.bidders)
143            .filter(function (e) { return e[1].bids > 0; })
144            .map(function (e) { return { name: e[0], score: scoreBidder(e[1]) }; })
145            .sort(function (a, b) { return b.score - a.score; });
146
147        var tier1Names = scored.slice(0, TIER1_MAX).map(function (x) { return x.name; });
148        var tier2Names = scored.slice(TIER1_MAX).map(function (x) { return x.name; });
149
150        var knownNames = {};
151        scored.forEach(function (x) { knownNames[x.name] = true; });
152        allBidderDefs.forEach(function (def) {
153            if (!knownNames[def[0]]) tier2Names.push(def[0]);
154        });
155
156        return { tier1: tier1Names, tier2: tier2Names };
157    }
158
159    function getBidsForNames(names) {
160        var nameSet = {};
161        names.forEach(function (n) { nameSet[n] = true; });
162        return allBidderDefs
163            .filter(function (def) { return nameSet[def[0]]; })
164            .map(function (def) { return { bidder: def[0], params: def[1] }; });
165    }
166
167    function randomBatchInterval() {
168        return BATCH_INTERVAL_MIN + Math.random() * (BATCH_INTERVAL_MAX - BATCH_INTERVAL_MIN);
169    }
170
171    function ensureAdUnit(divId) {
172        var s = getStats();
173        if (!s.adUnits[divId]) {
174            s.adUnits[divId] = {
175                consecutiveEmpty: 0,
176                recentWinCpms: [],
177                auctionsSinceTier1: 0,
178                currentFloor: null,      // null = uninitialized
179                pendingUpdates: [],      // buffered win/loss events awaiting batch flush
180                lastFloorUpdate: 0,      // timestamp of last batch flush
181                batchInterval: randomBatchInterval()
182            };
183        }
184        var unit = s.adUnits[divId];
185        // Back-fill new fields for units saved before this version
186        if (!Array.isArray(unit.pendingUpdates)) unit.pendingUpdates = [];
187        if (typeof unit.lastFloorUpdate !== 'number') unit.lastFloorUpdate = 0;
188        if (typeof unit.batchInterval !== 'number') unit.batchInterval = randomBatchInterval();
189        return unit;
190    }
191
192    // Returns average of stored win CPMs, or 0 if none
193    function avgCpm(unit) {
194        var cpms = unit.recentWinCpms;
195        if (!cpms || cpms.length === 0) return 0;
196        return cpms.reduce(function (a, b) { return a + b; }, 0) / cpms.length;
197    }
198
199    // Returns a jitter multiplier in [JITTER_MIN, JITTER_MAX]
200    function jitter() {
201        return JITTER_MIN + Math.random() * (JITTER_MAX - JITTER_MIN);
202    }
203
204    // Apply all buffered win/loss events to currentFloor and reset the queue.
205    // Uses jittered adjustments and caps at 80–85 % of avgCPM.
206    function flushPendingUpdates(unit) {
207        var countryMin = getCountryFloor();
208        var floor = unit.currentFloor;
209
210        if (floor === null) {
211            var avg = avgCpm(unit);
212            floor = avg > 0 ? avg * 0.70 : countryMin;
213        }
214
215        unit.pendingUpdates.forEach(function (update) {
216            var bump = jitter();
217            if (update.won) {
218                floor *= (1 + bump);
219            } else {
220                floor *= (1 - bump);
221            }
222        });
223
224        // Cap: never let the floor exceed 80–85 % of the user's average eCPM
225        var avg = avgCpm(unit);
226        if (avg > 0) {
227            var capFactor = FLOOR_CAP_MIN + Math.random() * (FLOOR_CAP_MAX - FLOOR_CAP_MIN);
228            floor = Math.min(floor, avg * capFactor);
229        }
230
231        unit.currentFloor = Math.max(countryMin, Math.min(FLOOR_MAX, parseFloat(floor.toFixed(4))));
232        unit.pendingUpdates = [];
233        unit.lastFloorUpdate = Date.now();
234        unit.batchInterval = randomBatchInterval();  // re-randomize next window
235    }
236
237    // Queue a win/loss event — does NOT touch currentFloor immediately.
238    // The floor only changes when the batch window expires (see computeFloor).
239    function updateFloor(divId, won) {
240        var unit = ensureAdUnit(divId);
241        unit.pendingUpdates.push({ won: won, ts: Date.now() });
242    }
243
244    // Returns the active floor to enforce for a given slot.
245    // Flushes the pending queue if the batch window has expired.
246    function computeFloor(divId, isTier2) {
247        var countryMin = getCountryFloor();
248        var s = getStats();
249
250        // ── Control group (10 %): always use a flat, static country-minimum floor ──
251        if (s.userGroup < CONTROL_GROUP_THRESHOLD) {
252            return countryMin;
253        }
254
255        // Tier 2 is already the fallback — use country minimum to maximise fill
256        if (isTier2) return countryMin;
257
258        var unit = ensureAdUnit(divId);
259
260        // Flush pending updates if the batch window has elapsed
261        if (unit.pendingUpdates.length > 0 &&
262            Date.now() - unit.lastFloorUpdate >= unit.batchInterval) {
263            flushPendingUpdates(unit);
264            save();
265        }
266
267        if (unit.currentFloor === null) {
268            // No flush has happened yet: compute an initial floor from avg eCPM
269            var avg = avgCpm(unit);
270            var floor = avg > 0 ? avg * 0.70 : countryMin;
271            // Still respect the cap even before the first flush
272            if (avg > 0) {
273                var capFactor = FLOOR_CAP_MIN + Math.random() * (FLOOR_CAP_MAX - FLOOR_CAP_MIN);
274                floor = Math.min(floor, avg * capFactor);
275            }
276            return Math.max(countryMin, Math.min(FLOOR_MAX, parseFloat(floor.toFixed(4))));
277        }
278        return unit.currentFloor;
279    }
280
281    function buildFloorData(isTier2, slots) {
282        var values = {};
283        slots.forEach(function (slot) {
284            values[slot.slotName + '|banner|*'] = computeFloor(slot.divId, isTier2);
285        });
286        return {
287            currency: 'EUR',
288            skipRate: 0,
289            modelVersion: 'dynamic-v3',
290            schema: { fields: ['gptSlot', 'mediaType', 'size'] },
291            values: values
292        };
293    }
294
295    function recordAuctionResults(bidResponses, slots) {
296        var s = getStats();
297        var now = Date.now();
298        var anyWin = false;
299
300        if (!bidResponses || typeof bidResponses !== 'object') {
301            slots.forEach(function (slot) {
302                var unit = ensureAdUnit(slot.divId);
303                unit.consecutiveEmpty++;
304                updateFloor(slot.divId, false);
305            });
306            save();
307            return false;
308        }
309
310        var seenUnits = {};
311
312        Object.keys(bidResponses).forEach(function (adUnitCode) {
313            seenUnits[adUnitCode] = true;
314            var response = bidResponses[adUnitCode];
315            var unit = ensureAdUnit(adUnitCode);
316            var bidsArr = (response && Array.isArray(response.bids)) ? response.bids : [];
317
318            if (bidsArr.length > 0) {
319                anyWin = true;
320                unit.consecutiveEmpty = 0;
321                unit.auctionsSinceTier1 = 0;
322
323                bidsArr.forEach(function (bid) {
324                    var bidderName = bid.bidderCode || bid.bidder || 'unknown';
325                    if (!s.bidders[bidderName]) {
326                        s.bidders[bidderName] = { wins: 0, bids: 0, totalCpm: 0, lastSeen: now };
327                    }
328                    var b = s.bidders[bidderName];
329                    b.bids++;
330                    b.lastSeen = now;
331                });
332
333                // Floor update for wins is handled by the bidWon event listener
334            } else {
335                unit.consecutiveEmpty++;
336                updateFloor(adUnitCode, false);
337            }
338        });
339
340        slots.forEach(function (slot) {
341            if (!seenUnits[slot.divId]) {
342                ensureAdUnit(slot.divId).consecutiveEmpty++;
343                updateFloor(slot.divId, false);
344            }
345        });
346
347        try {
348            if (typeof pbjs !== 'undefined' && typeof pbjs.getNoBids === 'function') {
349                pbjs.getNoBids().forEach(function (noBid) {
350                    var bidderName = noBid.bidder || noBid.bidderCode || 'unknown';
351                    if (!s.bidders[bidderName]) {
352                        s.bidders[bidderName] = { wins: 0, bids: 0, totalCpm: 0, lastSeen: now };
353                    }
354                    s.bidders[bidderName].bids++;
355                    s.bidders[bidderName].lastSeen = now;
356                });
357            }
358        } catch (e) { }
359
360        save();
361        return anyWin;
362    }
363
364    function decideTier() {
365        var s = getStats();
366        if (s.totalWins < LEARNING_THRESHOLD) return 'all';
367
368        var needsFallback = false;
369        for (var i = 0; i < adSlots.length; i++) {
370            if (ensureAdUnit(adSlots[i].divId).consecutiveEmpty >= EMPTY_STREAK_TIER2) {
371                needsFallback = true;
372                break;
373            }
374        }
375
376        if (needsFallback) {
377            auctionCount++;
378            if (auctionCount % EXPLORE_EVERY_N === 0) return 'tier1';
379            return 'tier2';
380        }
381        return 'tier1';
382    }
383
384    // ── Guaranteed GAM refresh — the nuclear option if everything else fails ──
385    function forceGAMRefresh() {
386        try {
387            pbjs.setTargetingForGPTAsync();
388            googletag.cmd.push(function () { googletag.pubads().refresh(); });
389        } catch (e) {
390            console.error('[BidManager] Even GAM refresh failed:', e);
391        }
392    }
393
394    window.BidManager = {
395
396        init: function (bidderDefs, slots) {
397            allBidderDefs = bidderDefs;
398            adSlots = slots;
399            try { stats = load(); } catch (e) { stats = freshStats(); }
400
401            // Reset streaks on every page load — always start with tier 1
402            var s = getStats();
403            Object.keys(s.adUnits).forEach(function (k) {
404                s.adUnits[k].consecutiveEmpty = 0;
405            });
406            save();
407
408            // Track actual wins via prebid event (status flags aren't set at bidsBackHandler time)
409            try {
410                pbjs.onEvent('bidWon', function (bid) {
411                    var ws = getStats();
412                    var now = Date.now();
413                    var bidderName = bid.bidderCode || bid.bidder || 'unknown';
414                    if (!ws.bidders[bidderName]) {
415                        ws.bidders[bidderName] = { wins: 0, bids: 0, totalCpm: 0, lastSeen: now };
416                    }
417                    var b = ws.bidders[bidderName];
418                    b.wins++;
419                    b.totalCpm += bid.cpm || 0;
420                    b.lastSeen = now;
421                    ws.totalWins++;
422
423                    var unit = ensureAdUnit(bid.adUnitCode);
424                    unit.recentWinCpms.push(bid.cpm || 0);
425                    if (unit.recentWinCpms.length > MAX_RECENT_CPMS) unit.recentWinCpms.shift();
426                    updateFloor(bid.adUnitCode, true);  // queued — not applied until batch flush
427                    save();
428
429                });
430            } catch (e) { }
431
432            try {
433                googletag.cmd.push(function () {
434                    googletag.pubads().addEventListener('slotRenderEnded', function (event) {
435                        if (!lastAuctionContext) return;
436                        var slotId = event.slot.getSlotElementId();
437                        var winner = lastAuctionContext.winners[slotId] || null;
438                        var source = event.isEmpty ? 'empty' : (winner ? 'ssp' : 'google');
439                        logAuction({
440                            userId: lastAuctionContext.userId,
441                            ts: new Date().toISOString(),
442                            country: lastAuctionContext.country,
443                            group: lastAuctionContext.group,
444                            phase: lastAuctionContext.phase,
445                            tier: lastAuctionContext.tier,
446                            floors: lastAuctionContext.floors,
447                            biddersInvited: lastAuctionContext.biddersInvited,
448                            biddersResponded: lastAuctionContext.biddersResponded,
449                            biddersSilent: lastAuctionContext.biddersSilent,
450                            adUnits: lastAuctionContext.adUnits,
451                            slotId: slotId,
452                            source: source,
453                            winner: winner,
454                            advertiserId: event.advertiserId || null,
455                            lineItemId: event.lineItemId || null,
456                            creativeId: event.creativeId || null
457                        });
458                    });
459                });
460            } catch (e) { }
461
462            var isControl = s.userGroup < CONTROL_GROUP_THRESHOLD;
463            console.log('[BidManager] Loaded: ' + s.totalWins + ' wins, ' +
464                Object.keys(s.bidders).length + ' bidders, ' +
465                'phase: ' + (s.totalWins < LEARNING_THRESHOLD ? 'learning' : 'tiered') + ', ' +
466                'country: ' + (window.userCountry || 'unknown') + ', ' +
467                'floor min: $' + getCountryFloor() + ', ' +
468                'group: ' + (isControl ? 'CONTROL (static floor)' : 'DYNAMIC (group=' + s.userGroup.toFixed(3) + ')'));
469        },
470
471        runAuction: function (timeout, activeSlots) {
472            // ── If anything at all goes wrong, fall back to original behavior ──
473            try {
474                if (typeof pbjs === 'undefined' || typeof googletag === 'undefined') return;
475
476                var slots = activeSlots || adSlots;
477                var tier = decideTier();
478                var tiers = classifyBidders();
479                var isTier2 = tier === 'tier2';
480
481                var bids;
482                if (tier === 'all') {
483                    bids = allBidderDefs.map(function (def) { return { bidder: def[0], params: def[1] }; });
484                } else if (tier === 'tier1') {
485                    bids = getBidsForNames(tiers.tier1);
486                } else {
487                    bids = getBidsForNames(tiers.tier2);
488                }
489
490                // Always include every bidder whose name contains "server" (case-insensitive),
491                // regardless of tier — add only those not already present in bids.
492                var presentBidders = {};
493                bids.forEach(function (b) { presentBidders[b.bidder] = true; });
494                allBidderDefs.forEach(function (def) {
495                    if (def[0].toLowerCase().indexOf('server') >= 0 && !presentBidders[def[0]]) {
496                        bids.push({ bidder: def[0], params: def[1] });
497                    }
498                });
499
500                try {
501                    pbjs.setConfig({
502                        floors: {
503                            enforcement: { enforceJS: true, enforcePBS: true, floorDeals: true },
504                            data: buildFloorData(isTier2, slots)
505                        }
506                    });
507                } catch (e) {
508                    console.error('[BidManager] Floor config error:', e);
509                }
510
511                try {
512                    slots.forEach(function (slot) { pbjs.removeAdUnit(slot.divId); });
513                } catch (e) { }
514
515                pbjs.addAdUnits(slots.map(function (slot) {
516                    return { code: slot.divId, mediaTypes: { banner: { sizes: slot.sizes } }, bids: bids };
517                }));
518
519                var floorSummary = slots.map(function (slot) {
520                    return slot.divId + ':$' + computeFloor(slot.divId, isTier2);
521                }).join(', ');
522                console.log('[BidManager] Auction: tier=' + tier + ', bidders=' + bids.length + ', slots=' + slots.length + ', floors=[' + floorSummary + ']');
523
524                var self = this;
525                pbjs.requestBids({
526                    timeout: timeout,
527                    bidsBackHandler: function (bidResponses) {
528                        var anyWin = false;
529                        try {
530                            anyWin = recordAuctionResults(bidResponses, slots);
531                        } catch (e) {
532                            console.error('[BidManager] Record error:', e);
533                        }
534
535                        try {
536                            var ls = getStats();
537                            var isCtrl = ls.userGroup < CONTROL_GROUP_THRESHOLD;
538                            var invitedBidders = bids.map(function (b) { return b.bidder; });
539                            var respondedBidders = [];
540                            var silentBidders = [];
541                            var adUnitLogs = [];
542
543                            Object.keys(bidResponses || {}).forEach(function (code) {
544                                var resp = bidResponses[code];
545                                var bidsArr = (resp && Array.isArray(resp.bids)) ? resp.bids : [];
546                                var unitBids = bidsArr.map(function (bid) {
547                                    var name = bid.bidderCode || bid.bidder || 'unknown';
548                                    if (respondedBidders.indexOf(name) < 0) respondedBidders.push(name);
549                                    return { bidder: name, cpm: bid.cpm, currency: bid.currency || 'EUR' };
550                                });
551                                adUnitLogs.push({ adUnit: code, bids: unitBids });
552                            });
553
554                            invitedBidders.forEach(function (name) {
555                                if (respondedBidders.indexOf(name) < 0) silentBidders.push(name);
556                            });
557
558                            var winners = {};
559                            if (anyWin) {
560                                adUnitLogs.forEach(function (u) {
561                                    var top = null;
562                                    u.bids.forEach(function (b) {
563                                        if (!top || b.cpm > top.cpm) top = b;
564                                    });
565                                    if (top) winners[u.adUnit] = { bidder: top.bidder, cpm: top.cpm, currency: top.currency };
566                                });
567                            }
568
569                            lastAuctionContext = {
570                                userId: getUserId(),
571                                country: window.userCountry || 'unknown',
572                                group: isCtrl ? 'control' : 'dynamic',
573                                phase: ls.totalWins < LEARNING_THRESHOLD ? 'learning' : 'tiered',
574                                tier: tier,
575                                floors: slots.map(function (slot) {
576                                    return { divId: slot.divId, floor: computeFloor(slot.divId, isTier2) };
577                                }),
578                                biddersInvited: invitedBidders,
579                                biddersResponded: respondedBidders,
580                                biddersSilent: silentBidders,
581                                adUnits: adUnitLogs,
582                                anyWin: anyWin,
583                                winners: winners
584                            };
585                        } catch (e) { }
586
587                        if (anyWin) {
588                            forceGAMRefresh();
589                        } else if (tier === 'tier1' && getStats().totalWins >= LEARNING_THRESHOLD) {
590                            console.log('[BidManager] Tier 1 empty → Tier 2');
591                            self.runAuction(timeout, activeSlots);
592                        } else {
593                            forceGAMRefresh();
594                        }
595                    }
596                });
597
598            } catch (e) {
599                // ── TOTAL FALLBACK: something crashed, just run GAM directly ──
600                console.error('[BidManager] Auction crashed, forcing GAM refresh:', e);
601                forceGAMRefresh();
602            }
603        },
604
605        getStats: function () {
606            var s = getStats();
607            var tiers = s.totalWins >= LEARNING_THRESHOLD ? classifyBidders() : null;
608            var countryMin = getCountryFloor();
609            var isControl = s.userGroup < CONTROL_GROUP_THRESHOLD;
610            return {
611                phase: s.totalWins < LEARNING_THRESHOLD ? 'learning' : 'tiered',
612                totalWins: s.totalWins,
613                country: window.userCountry || 'unknown',
614                countryFloor: countryMin,
615                group: isControl ? 'control' : 'dynamic',
616                userGroup: s.userGroup,
617                bidders: Object.entries(s.bidders)
618                    .map(function (e) {
619                        return {
620                            name: e[0], wins: e[1].wins, bids: e[1].bids,
621                            avgCpm: e[1].wins > 0 ? (e[1].totalCpm / e[1].wins).toFixed(4) : '0',
622                            score: scoreBidder(e[1]).toFixed(4),
623                            tier: tiers ? (tiers.tier1.indexOf(e[0]) >= 0 ? 1 : 2) : '-'
624                        };
625                    })
626                    .sort(function (a, b) { return parseFloat(b.score) - parseFloat(a.score); }),
627                adUnits: Object.entries(s.adUnits).map(function (e) {
628                    var unit = e[1];
629                    var pendingCount = Array.isArray(unit.pendingUpdates) ? unit.pendingUpdates.length : 0;
630                    var msUntilFlush = unit.lastFloorUpdate > 0
631                        ? Math.max(0, (unit.lastFloorUpdate + (unit.batchInterval || BATCH_INTERVAL_MIN)) - Date.now())
632                        : 0;
633                    return {
634                        divId: e[0],
635                        consecutiveEmpty: unit.consecutiveEmpty,
636                        currentFloor: unit.currentFloor !== null ? unit.currentFloor : 'unset',
637                        floorT1: computeFloor(e[0], false),
638                        floorT2: computeFloor(e[0], true),
639                        recentWins: unit.recentWinCpms.length,
640                        pendingUpdates: pendingCount,
641                        nextFlushInMs: msUntilFlush
642                    };
643                })
644            };
645        },
646
647        reset: function () { stats = freshStats(); save(); console.log('[BidManager] Reset'); },
648
649        report: function () {
650            var s = this.getStats();
651            console.group('[BidManager] Phase: ' + s.phase + ', Wins: ' + s.totalWins +
652                ', Country: ' + s.country + ', Floor min: $' + s.countryFloor +
653                ', Group: ' + s.group);
654            console.table(s.bidders);
655            console.table(s.adUnits);
656            console.groupEnd();
657        }
658    };
659})();

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.