Web Statistics Monitor [BETA]

Advanced real-time web page and video statistics monitoring

// ==UserScript==
// @name         Web Statistics Monitor [BETA]
// @namespace    Violentmonkey Scripts
// @version      1.0
// @description  Advanced real-time web page and video statistics monitoring
// @author       DXRK1E
// @match       *://*/*
// @run-at       document-end
// @license MIT
// ==/UserScript==

(function() {
    'use strict';

    class WebStatsMonitor {
        #popup;
        #toggleButton;
        #statsInterval;
        #videoStatsInterval;
        #lastSnapshot = {};
        #styles = `
            .scanneri-popup {
                position: fixed;
                bottom: 20px;
                left: 20px;
                width: 400px;
                max-height: 80vh;
                background-color: rgba(28, 28, 35, 0.95);
                color: #f0f0f0;
                padding: 15px;
                border-radius: 12px;
                box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
                font-family: system-ui, -apple-system, sans-serif;
                font-size: 14px;
                z-index: 999999;
                display: none;
                overflow-y: auto;
                backdrop-filter: blur(5px);
            }
            .scanneri-header {
                display: flex;
                justify-content: space-between;
                align-items: center;
                margin-bottom: 15px;
            }
            .scanneri-title {
                font-size: 16px;
                font-weight: 600;
                margin: 0;
            }
            .scanneri-controls {
                display: flex;
                gap: 8px;
            }
            .scanneri-button {
                background-color: rgba(255, 255, 255, 0.1);
                border: none;
                color: #fff;
                padding: 5px 10px;
                border-radius: 6px;
                cursor: pointer;
                font-size: 12px;
                transition: background-color 0.2s;
            }
            .scanneri-button:hover {
                background-color: rgba(255, 255, 255, 0.2);
            }
            .scanneri-content {
                margin-top: 10px;
            }
            .scanneri-list {
                list-style: none;
                padding: 0;
                margin: 0;
            }
            .scanneri-item {
                padding: 8px;
                margin: 5px 0;
                background-color: rgba(255, 255, 255, 0.05);
                border-radius: 6px;
                word-break: break-all;
                font-size: 12px;
                transition: background-color 0.2s;
                display: flex;
                flex-direction: column;
                gap: 4px;
                position: relative;
            }
            .scanneri-item:hover {
                background-color: rgba(255, 255, 255, 0.1);
            }
            .scanneri-item-source {
                color: #fff;
            }
            .scanneri-item-info {
                color: rgba(255, 255, 255, 0.6);
                font-size: 10px;
                display: flex;
                gap: 8px;
                align-items: center;
                flex-wrap: wrap;
            }
            .scanneri-item-tag {
                background-color: rgba(255, 255, 255, 0.1);
                padding: 2px 6px;
                border-radius: 4px;
                font-size: 9px;
            }
            .scanneri-section {
                border-top: 1px solid rgba(255, 255, 255, 0.1);
                padding-top: 10px;
                margin-top: 10px;
            }
            .scanneri-stat-value {
                font-family: monospace;
                color: #4CAF50;
            }
            .scanneri-stat-change {
                font-size: 9px;
                padding: 1px 4px;
                border-radius: 3px;
                margin-left: 5px;
            }
            .scanneri-stat-increase {
                background-color: rgba(76, 175, 80, 0.2);
                color: #4CAF50;
            }
            .scanneri-stat-decrease {
                background-color: rgba(244, 67, 54, 0.2);
                color: #F44336;
            }`;

        constructor() {
            this.#initializeUI();
            this.#setupMutationObserver();
            this.startMonitoring();
        }

        #initializeUI() {
            this.#createStyles();
            this.#createPopup();
            this.#createToggleButton();
            this.#setupKeyboardShortcut();
        }

        #createStyles() {
            const styleSheet = document.createElement('style');
            styleSheet.textContent = this.#styles;
            document.head.appendChild(styleSheet);
        }

        #createPopup() {
            this.#popup = document.createElement('div');
            this.#popup.className = 'scanneri-popup';
            document.body.appendChild(this.#popup);
        }

        #createToggleButton() {
            this.#toggleButton = document.createElement('button');
            this.#toggleButton.className = 'scanneri-button';
            this.#toggleButton.style.cssText = `
                position: fixed;
                bottom: 20px;
                left: 20px;
                z-index: 999998;
                padding: 8px 15px;
                background-color: rgba(28, 28, 35, 0.95);
                box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
            `;
            this.#toggleButton.textContent = '📊 Web Stats';
            this.#toggleButton.addEventListener('click', () => this.toggle());
            document.body.appendChild(this.#toggleButton);
        }

        #setupKeyboardShortcut() {
            document.addEventListener('keydown', (e) => {
                if (e.ctrlKey && e.shiftKey && e.key === 'S') {
                    this.toggle();
                }
            });
        }

        #setupMutationObserver() {
            const observer = new MutationObserver(() => {
                if (this.#popup.style.display === 'block') {
                    this.updateStats();
                }
            });

            observer.observe(document.body, {
                childList: true,
                subtree: true,
                attributes: true,
                characterData: true
            });
        }

        async #getPerformanceMetrics() {
            const navigation = performance.getEntriesByType('navigation')[0];
            const paint = performance.getEntriesByType('paint');
            const memory = performance.memory ? {
                usedJSHeapSize: Math.round(performance.memory.usedJSHeapSize / (1024 * 1024)),
                totalJSHeapSize: Math.round(performance.memory.totalJSHeapSize / (1024 * 1024))
            } : null;

            return {
                pageLoad: Math.round(navigation.loadEventEnd),
                firstPaint: Math.round(paint.find(p => p.name === 'first-paint')?.startTime || 0),
                firstContentfulPaint: Math.round(paint.find(p => p.name === 'first-contentful-paint')?.startTime || 0),
                memory
            };
        }

        #getNetworkStats() {
            const resources = performance.getEntriesByType('resource');
            const totalBytes = resources.reduce((acc, resource) => acc + (resource.transferSize || 0), 0);
            const totalRequests = resources.length;

            const typeStats = resources.reduce((acc, resource) => {
                const type = resource.initiatorType || 'other';
                acc[type] = (acc[type] || 0) + 1;
                return acc;
            }, {});

            return {
                totalBytes: Math.round(totalBytes / 1024),
                totalRequests,
                typeStats
            };
        }

        #getDOMStats() {
            const elements = document.getElementsByTagName('*');
            const elementCounts = {};

            for (const element of elements) {
                const tagName = element.tagName.toLowerCase();
                elementCounts[tagName] = (elementCounts[tagName] || 0) + 1;
            }

            return {
                totalElements: elements.length,
                elementCounts,
                documentHeight: Math.max(
                    document.body.scrollHeight,
                    document.documentElement.scrollHeight,
                    document.body.offsetHeight,
                    document.documentElement.offsetHeight
                ),
                documentWidth: Math.max(
                    document.body.scrollWidth,
                    document.documentElement.scrollWidth,
                    document.body.offsetWidth,
                    document.documentElement.offsetWidth
                )
            };
        }

        #getVideoStats() {
            const videoElements = document.getElementsByTagName('video');
            const videoStats = [];

            for (const video of videoElements) {
                videoStats.push({
                    duration: Math.round(video.duration) || 0,
                    currentTime: Math.round(video.currentTime) || 0,
                    playbackRate: video.playbackRate,
                    buffered: video.buffered.length ?
                        Math.round((video.buffered.end(video.buffered.length - 1) / video.duration) * 100) : 0,
                    resolution: `${video.videoWidth}x${video.videoHeight}`,
                    volume: Math.round(video.volume * 100),
                    state: video.paused ? 'paused' : 'playing',
                    frameDropped: video.getVideoPlaybackQuality?.()?.droppedVideoFrames || 0
                });
            }

            return videoStats;
        }

        #formatBytes(bytes) {
            if (bytes === 0) return '0 B';
            const k = 1024;
            const sizes = ['B', 'KB', 'MB', 'GB'];
            const i = Math.floor(Math.log(bytes) / Math.log(k));
            return `${parseFloat((bytes / Math.pow(k, i)).toFixed(2))} ${sizes[i]}`;
        }

        #calculateChange(current, previous) {
            if (!previous) return null;
            const change = current - previous;
            return change !== 0 ? change : null;
        }

        #formatChange(change) {
            if (change === null) return '';
            const sign = change > 0 ? '+' : '';
            const className = change > 0 ? 'scanneri-stat-increase' : 'scanneri-stat-decrease';
            return `<span class="scanneri-stat-change ${className}">${sign}${change}</span>`;
        }

        async updateStats() {
            const performance = await this.#getPerformanceMetrics();
            const network = this.#getNetworkStats();
            const dom = this.#getDOMStats();
            const videoStats = this.#getVideoStats();

            const currentSnapshot = {
                network: network.totalRequests,
                dom: dom.totalElements
            };

            const changes = {
                network: this.#calculateChange(currentSnapshot.network, this.#lastSnapshot.network),
                dom: this.#calculateChange(currentSnapshot.dom, this.#lastSnapshot.dom)
            };

            this.#lastSnapshot = currentSnapshot;

            const content = `
                <div class="scanneri-header">
                    <h2 class="scanneri-title">Web Statistics Monitor</h2>
                    <div class="scanneri-controls">
                        <button class="scanneri-button" id="scanneri-close">✕</button>
                    </div>
                </div>

                <div class="scanneri-content">
                    <div class="scanneri-section">
                        <h3>Performance Metrics</h3>
                        <div class="scanneri-item">
                            <div class="scanneri-item-info">
                                <span>Page Load: <span class="scanneri-stat-value">${performance.pageLoad}ms</span></span>
                                <span>First Paint: <span class="scanneri-stat-value">${performance.firstPaint}ms</span></span>
                                <span>First Contentful Paint: <span class="scanneri-stat-value">${performance.firstContentfulPaint}ms</span></span>
                                ${performance.memory ? `
                                    <span>Memory Usage: <span class="scanneri-stat-value">${performance.memory.usedJSHeapSize}MB / ${performance.memory.totalJSHeapSize}MB</span></span>
                                ` : ''}
                            </div>
                        </div>
                    </div>

                    <div class="scanneri-section">
                        <h3>Network Statistics</h3>
                        <div class="scanneri-item">
                            <div class="scanneri-item-info">
                                <span>Total Requests: <span class="scanneri-stat-value">${network.totalRequests}</span>${this.#formatChange(changes.network)}</span>
                                <span>Total Transfer: <span class="scanneri-stat-value">${this.#formatBytes(network.totalBytes * 1024)}</span></span>
                            </div>
                            <div class="scanneri-item-info">
                                ${Object.entries(network.typeStats).map(([type, count]) =>
                                    `<span class="scanneri-item-tag">${type}: ${count}</span>`
                                ).join('')}
                            </div>
                        </div>
                    </div>

                    <div class="scanneri-section">
                        <h3>DOM Statistics</h3>
                        <div class="scanneri-item">
                            <div class="scanneri-item-info">
                                <span>Total Elements: <span class="scanneri-stat-value">${dom.totalElements}</span>${this.#formatChange(changes.dom)}</span>
                                <span>Document Size: <span class="scanneri-stat-value">${dom.documentWidth}x${dom.documentHeight}</span></span>
                            </div>
                            <div class="scanneri-item-info">
                                ${Object.entries(dom.elementCounts)
                                    .sort((a, b) => b[1] - a[1])
                                    .slice(0, 10)
                                    .map(([tag, count]) =>
                                        `<span class="scanneri-item-tag">${tag}: ${count}</span>`
                                    ).join('')}
                            </div>
                        </div>
                    </div>

                    ${videoStats.length > 0 ? `
                        <div class="scanneri-section">
                            <h3>Video Statistics</h3>
                            ${videoStats.map((stats, index) => `
                                <div class="scanneri-item">
                                    <div class="scanneri-item-info">
                                        <span>Video #${index + 1}</span>
                                        <span class="scanneri-item-tag">Resolution: ${stats.resolution}</span>
                                        <span class="scanneri-item-tag">State: ${stats.state}</span>
                                        <span class="scanneri-item-tag">Duration: ${stats.duration}s</span>
                                    </div>
                                    <div class="scanneri-item-info">
                                        <span class="scanneri-item-tag">Current Time: ${stats.currentTime}s</span>
                                        <span class="scanneri-item-tag">Buffer: ${stats.buffered}%</span>
                                        <span class="scanneri-item-tag">Speed: ${stats.playbackRate}x</span>
                                        <span class="scanneri-item-tag">Volume: ${stats.volume}%</span>
                                        <span class="scanneri-item-tag">Dropped Frames: ${stats.frameDropped}</span>
                                    </div>
                                </div>
                            `).join('')}
                        </div>
                    ` : ''}

                    <div class="scanneri-section">
                        <h3>Resource Timing</h3>
                        <div class="scanneri-item">
                            ${this.#generateResourceTimingChart()}
                        </div>
                    </div>
                </div>
            `;

            this.#popup.innerHTML = content;
            this.#setupEventListeners();
        }

        #generateResourceTimingChart() {
            const resources = performance.getEntriesByType('resource');
            const maxDuration = Math.max(...resources.map(r => r.duration));

            return `
                <div style="font-size: 10px;">
                    ${resources.slice(0, 10).map(resource => {
                        const width = (resource.duration / maxDuration) * 100;
                        const name = resource.name.split('/').pop();
                        return `
                            <div style="margin: 4px 0;">
                                <div style="display: flex; align-items: center; gap: 8px;">
                                    <div style="width: 100px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">${name}</div>
                                    <div style="flex-grow: 1; background: rgba(255,255,255,0.1); height: 6px; border-radius: 3px;">
                                        <div style="width: ${width}%; background: #4CAF50; height: 100%; border-radius: 3px;"></div>
                                    </div>
                                    <div style="width: 60px; text-align: right;">${Math.round(resource.duration)}ms</div>
                                </div>
                            </div>
                        `;
                    }).join('')}
                </div>
            `;
        }

        #setupEventListeners() {
            this.#popup.querySelector('#scanneri-close')?.addEventListener('click', () => this.hide());

            // Add click handlers for expandable sections
            this.#popup.querySelectorAll('.scanneri-section h3').forEach(header => {
                header.style.cursor = 'pointer';
                header.addEventListener('click', () => {
                    const content = header.nextElementSibling;
                    content.style.display = content.style.display === 'none' ? 'block' : 'none';
                });
            });
        }

        startMonitoring() {
            // Update stats every second
            this.#statsInterval = setInterval(() => {
                if (this.#popup.style.display === 'block') {
                    this.updateStats();
                }
            }, 1000);

            // Monitor video elements more frequently
            this.#videoStatsInterval = setInterval(() => {
                if (this.#popup.style.display === 'block' && document.getElementsByTagName('video').length > 0) {
                    this.updateStats();
                }
            }, 250);
        }

        stopMonitoring() {
            clearInterval(this.#statsInterval);
            clearInterval(this.#videoStatsInterval);
        }

        show() {
            this.#popup.style.display = 'block';
            this.#toggleButton.style.display = 'none';
            this.updateStats();
        }

        hide() {
            this.#popup.style.display = 'none';
            this.#toggleButton.style.display = 'block';
        }

        toggle() {
            if (this.#popup.style.display === 'none') {
                this.show();
            } else {
                this.hide();
            }
        }

        #addCustomMetrics() {
            // Add custom performance marks and measures
            performance.mark('stats-monitor-start');

            // Track page interactions
            let interactionCount = 0;
            document.addEventListener('click', () => {
                interactionCount++;
                performance.mark(`user-interaction-${interactionCount}`);
            });

            // Track AJAX requests
            const originalXHR = window.XMLHttpRequest;
            window.XMLHttpRequest = function() {
                const xhr = new originalXHR();
                const start = performance.now();

                xhr.addEventListener('loadend', () => {
                    const duration = performance.now() - start;
                    performance.mark(`xhr-${start}`);
                    performance.measure(`XHR Request`, `xhr-${start}`, undefined, { duration });
                });

                return xhr;
            };

            // Track fetch requests
            const originalFetch = window.fetch;
            window.fetch = function() {
                const start = performance.now();
                return originalFetch.apply(this, arguments)
                    .then(response => {
                        const duration = performance.now() - start;
                        performance.mark(`fetch-${start}`);
                        performance.measure(`Fetch Request`, `fetch-${start}`, undefined, { duration });
                        return response;
                    });
            };
        }
    }

    // Initialize the monitor when the page is ready
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', () => new WebStatsMonitor());
    } else {
        new WebStatsMonitor();
    }
})();