// ==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();
}
})();