mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-24 13:31:09 -05:00
- Introduced a comprehensive Secret Management Guide detailing best practices, secret classification, development setup, production management, rotation procedures, and emergency protocols. - Implemented a client-side performance monitoring script to track various metrics including page load performance, paint metrics, layout shifts, and memory usage. - Enhanced search accessibility with keyboard navigation support for search results, ensuring compliance with WCAG standards and improving user experience.
412 lines
14 KiB
JavaScript
412 lines
14 KiB
JavaScript
/**
|
|
* ThrillWiki Client-Side Performance Monitoring
|
|
*
|
|
* This module provides comprehensive client-side performance metrics collection
|
|
* and monitoring. It tracks:
|
|
* - Page load performance (Navigation Timing API)
|
|
* - Paint metrics (First Paint, First Contentful Paint)
|
|
* - Layout shifts (Cumulative Layout Shift)
|
|
* - Largest Contentful Paint
|
|
* - First Input Delay
|
|
* - Memory usage (Chrome only)
|
|
* - Long tasks (> 50ms)
|
|
* - Resource timing
|
|
*
|
|
* Usage:
|
|
* Include this script after DOMContentLoaded.
|
|
* Metrics are logged to console in development and can be sent to a backend endpoint.
|
|
*/
|
|
|
|
(function() {
|
|
'use strict';
|
|
|
|
// Configuration
|
|
const CONFIG = {
|
|
enableConsoleLogging: true, // Log to console in development
|
|
enableBeacon: false, // Send metrics to backend (set endpoint below)
|
|
beaconEndpoint: '/api/v1/metrics/performance/',
|
|
memoryCheckInterval: 30000, // Check memory every 30 seconds
|
|
memoryWarningThreshold: 0.9, // Warn at 90% memory usage
|
|
longTaskThreshold: 50, // Long task threshold in ms
|
|
reportDelay: 3000, // Delay before reporting metrics
|
|
};
|
|
|
|
// Metrics storage
|
|
const metrics = {
|
|
navigation: {},
|
|
paint: {},
|
|
resources: [],
|
|
longTasks: [],
|
|
layoutShifts: [],
|
|
largestContentfulPaint: null,
|
|
firstInputDelay: null,
|
|
memory: [],
|
|
errors: [],
|
|
};
|
|
|
|
/**
|
|
* Collect Navigation Timing metrics
|
|
*/
|
|
function collectNavigationTiming() {
|
|
if (!performance.getEntriesByType) return;
|
|
|
|
const navEntries = performance.getEntriesByType('navigation');
|
|
if (navEntries.length === 0) return;
|
|
|
|
const nav = navEntries[0];
|
|
metrics.navigation = {
|
|
// DNS lookup time
|
|
dnsLookup: nav.domainLookupEnd - nav.domainLookupStart,
|
|
// TCP connection time
|
|
tcpConnect: nav.connectEnd - nav.connectStart,
|
|
// TLS handshake time (if HTTPS)
|
|
tlsHandshake: nav.secureConnectionStart > 0
|
|
? nav.connectEnd - nav.secureConnectionStart
|
|
: 0,
|
|
// Time to First Byte (TTFB)
|
|
ttfb: nav.responseStart - nav.requestStart,
|
|
// Response download time
|
|
responseTime: nav.responseEnd - nav.responseStart,
|
|
// DOM parsing time
|
|
domParsing: nav.domContentLoadedEventEnd - nav.domInteractive,
|
|
// DOM Content Loaded
|
|
domContentLoaded: nav.domContentLoadedEventEnd - nav.startTime,
|
|
// Full page load
|
|
loadComplete: nav.loadEventEnd - nav.startTime,
|
|
// Redirect time (if any)
|
|
redirectTime: nav.redirectEnd - nav.redirectStart,
|
|
// Transfer size
|
|
transferSize: nav.transferSize,
|
|
// Encoded body size
|
|
encodedBodySize: nav.encodedBodySize,
|
|
// Decoded body size
|
|
decodedBodySize: nav.decodedBodySize,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Collect Paint Timing metrics
|
|
*/
|
|
function collectPaintTiming() {
|
|
if (!performance.getEntriesByType) return;
|
|
|
|
const paintEntries = performance.getEntriesByType('paint');
|
|
paintEntries.forEach(entry => {
|
|
if (entry.name === 'first-paint') {
|
|
metrics.paint.firstPaint = entry.startTime;
|
|
} else if (entry.name === 'first-contentful-paint') {
|
|
metrics.paint.firstContentfulPaint = entry.startTime;
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Observe Largest Contentful Paint
|
|
*/
|
|
function observeLCP() {
|
|
if (!('PerformanceObserver' in window)) return;
|
|
|
|
try {
|
|
const lcpObserver = new PerformanceObserver((entryList) => {
|
|
const entries = entryList.getEntries();
|
|
const lastEntry = entries[entries.length - 1];
|
|
metrics.largestContentfulPaint = {
|
|
time: lastEntry.startTime,
|
|
element: lastEntry.element ? lastEntry.element.tagName : 'unknown',
|
|
size: lastEntry.size,
|
|
url: lastEntry.url || null,
|
|
};
|
|
});
|
|
lcpObserver.observe({ type: 'largest-contentful-paint', buffered: true });
|
|
} catch (e) {
|
|
// LCP not supported
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Observe First Input Delay
|
|
*/
|
|
function observeFID() {
|
|
if (!('PerformanceObserver' in window)) return;
|
|
|
|
try {
|
|
const fidObserver = new PerformanceObserver((entryList) => {
|
|
const entries = entryList.getEntries();
|
|
entries.forEach(entry => {
|
|
if (!metrics.firstInputDelay) {
|
|
metrics.firstInputDelay = {
|
|
delay: entry.processingStart - entry.startTime,
|
|
eventType: entry.name,
|
|
target: entry.target ? entry.target.tagName : 'unknown',
|
|
};
|
|
}
|
|
});
|
|
});
|
|
fidObserver.observe({ type: 'first-input', buffered: true });
|
|
} catch (e) {
|
|
// FID not supported
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Observe Cumulative Layout Shift
|
|
*/
|
|
function observeCLS() {
|
|
if (!('PerformanceObserver' in window)) return;
|
|
|
|
let clsValue = 0;
|
|
let sessionValue = 0;
|
|
let sessionEntries = [];
|
|
|
|
try {
|
|
const clsObserver = new PerformanceObserver((entryList) => {
|
|
const entries = entryList.getEntries();
|
|
entries.forEach(entry => {
|
|
// Only count layout shifts without recent user input
|
|
if (!entry.hadRecentInput) {
|
|
const firstSessionEntry = sessionEntries[0];
|
|
const lastSessionEntry = sessionEntries[sessionEntries.length - 1];
|
|
|
|
// Start a new session if gap > 1 second or session > 5 seconds
|
|
if (sessionValue &&
|
|
(entry.startTime - lastSessionEntry.startTime > 1000 ||
|
|
entry.startTime - firstSessionEntry.startTime > 5000)) {
|
|
clsValue = Math.max(clsValue, sessionValue);
|
|
sessionValue = entry.value;
|
|
sessionEntries = [entry];
|
|
} else {
|
|
sessionValue += entry.value;
|
|
sessionEntries.push(entry);
|
|
}
|
|
|
|
metrics.layoutShifts.push({
|
|
value: entry.value,
|
|
time: entry.startTime,
|
|
sources: entry.sources ? entry.sources.length : 0,
|
|
});
|
|
}
|
|
});
|
|
});
|
|
clsObserver.observe({ type: 'layout-shift', buffered: true });
|
|
} catch (e) {
|
|
// CLS not supported
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Observe Long Tasks
|
|
*/
|
|
function observeLongTasks() {
|
|
if (!('PerformanceObserver' in window)) return;
|
|
|
|
try {
|
|
const longTaskObserver = new PerformanceObserver((entryList) => {
|
|
const entries = entryList.getEntries();
|
|
entries.forEach(entry => {
|
|
if (entry.duration > CONFIG.longTaskThreshold) {
|
|
metrics.longTasks.push({
|
|
duration: entry.duration,
|
|
startTime: entry.startTime,
|
|
name: entry.name,
|
|
});
|
|
}
|
|
});
|
|
});
|
|
longTaskObserver.observe({ entryTypes: ['longtask'] });
|
|
} catch (e) {
|
|
// Long tasks not supported
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Monitor memory usage (Chrome only)
|
|
*/
|
|
function monitorMemory() {
|
|
if (!performance.memory) return;
|
|
|
|
const checkMemory = () => {
|
|
const memoryInfo = performance.memory;
|
|
const usedRatio = memoryInfo.usedJSHeapSize / memoryInfo.jsHeapSizeLimit;
|
|
|
|
const memorySnapshot = {
|
|
timestamp: Date.now(),
|
|
usedHeapSize: memoryInfo.usedJSHeapSize,
|
|
totalHeapSize: memoryInfo.totalJSHeapSize,
|
|
heapSizeLimit: memoryInfo.jsHeapSizeLimit,
|
|
usedRatio: usedRatio,
|
|
};
|
|
|
|
metrics.memory.push(memorySnapshot);
|
|
|
|
// Keep only last 10 snapshots
|
|
if (metrics.memory.length > 10) {
|
|
metrics.memory.shift();
|
|
}
|
|
|
|
// Warn if memory usage is high
|
|
if (usedRatio > CONFIG.memoryWarningThreshold) {
|
|
console.warn('ThrillWiki Performance: High memory usage detected', {
|
|
usedPercent: (usedRatio * 100).toFixed(1) + '%',
|
|
usedMB: (memoryInfo.usedJSHeapSize / 1024 / 1024).toFixed(2) + ' MB',
|
|
limitMB: (memoryInfo.jsHeapSizeLimit / 1024 / 1024).toFixed(2) + ' MB',
|
|
});
|
|
}
|
|
};
|
|
|
|
// Initial check
|
|
checkMemory();
|
|
|
|
// Periodic checks
|
|
setInterval(checkMemory, CONFIG.memoryCheckInterval);
|
|
}
|
|
|
|
/**
|
|
* Collect resource timing
|
|
*/
|
|
function collectResourceTiming() {
|
|
if (!performance.getEntriesByType) return;
|
|
|
|
const resources = performance.getEntriesByType('resource');
|
|
metrics.resources = resources.slice(0, 50).map(resource => ({
|
|
name: resource.name.split('?')[0], // Remove query params
|
|
type: resource.initiatorType,
|
|
duration: resource.duration,
|
|
transferSize: resource.transferSize,
|
|
startTime: resource.startTime,
|
|
}));
|
|
}
|
|
|
|
/**
|
|
* Generate performance summary
|
|
*/
|
|
function generateSummary() {
|
|
const summary = {
|
|
timestamp: new Date().toISOString(),
|
|
url: window.location.href,
|
|
userAgent: navigator.userAgent,
|
|
navigation: metrics.navigation,
|
|
paint: metrics.paint,
|
|
lcp: metrics.largestContentfulPaint,
|
|
fid: metrics.firstInputDelay,
|
|
cls: metrics.layoutShifts.reduce((sum, shift) => sum + shift.value, 0),
|
|
longTaskCount: metrics.longTasks.length,
|
|
longTaskTotalTime: metrics.longTasks.reduce((sum, task) => sum + task.duration, 0),
|
|
resourceCount: metrics.resources.length,
|
|
totalTransferSize: metrics.resources.reduce((sum, r) => sum + (r.transferSize || 0), 0),
|
|
};
|
|
|
|
// Calculate Web Vitals ratings
|
|
summary.webVitals = {
|
|
lcp: rateMetric('lcp', summary.lcp?.time),
|
|
fid: rateMetric('fid', summary.fid?.delay),
|
|
cls: rateMetric('cls', summary.cls),
|
|
};
|
|
|
|
return summary;
|
|
}
|
|
|
|
/**
|
|
* Rate a metric according to Web Vitals thresholds
|
|
*/
|
|
function rateMetric(metric, value) {
|
|
if (value === null || value === undefined) return 'unknown';
|
|
|
|
const thresholds = {
|
|
lcp: { good: 2500, needsImprovement: 4000 },
|
|
fid: { good: 100, needsImprovement: 300 },
|
|
cls: { good: 0.1, needsImprovement: 0.25 },
|
|
};
|
|
|
|
const threshold = thresholds[metric];
|
|
if (!threshold) return 'unknown';
|
|
|
|
if (value <= threshold.good) return 'good';
|
|
if (value <= threshold.needsImprovement) return 'needs-improvement';
|
|
return 'poor';
|
|
}
|
|
|
|
/**
|
|
* Log metrics to console
|
|
*/
|
|
function logMetrics() {
|
|
if (!CONFIG.enableConsoleLogging) return;
|
|
|
|
const summary = generateSummary();
|
|
|
|
console.group('ThrillWiki Performance Metrics');
|
|
console.log('Page Load:', summary.navigation.loadComplete?.toFixed(0) + 'ms');
|
|
console.log('TTFB:', summary.navigation.ttfb?.toFixed(0) + 'ms');
|
|
console.log('First Paint:', summary.paint.firstPaint?.toFixed(0) + 'ms');
|
|
console.log('First Contentful Paint:', summary.paint.firstContentfulPaint?.toFixed(0) + 'ms');
|
|
console.log('Largest Contentful Paint:', summary.lcp?.time?.toFixed(0) + 'ms', `(${summary.webVitals.lcp})`);
|
|
console.log('First Input Delay:', summary.fid?.delay?.toFixed(0) + 'ms', `(${summary.webVitals.fid})`);
|
|
console.log('Cumulative Layout Shift:', summary.cls?.toFixed(3), `(${summary.webVitals.cls})`);
|
|
console.log('Long Tasks:', summary.longTaskCount, `(${summary.longTaskTotalTime.toFixed(0)}ms total)`);
|
|
console.log('Resources:', summary.resourceCount, `(${(summary.totalTransferSize / 1024).toFixed(0)}KB)`);
|
|
console.groupEnd();
|
|
}
|
|
|
|
/**
|
|
* Send metrics to backend
|
|
*/
|
|
function sendMetrics() {
|
|
if (!CONFIG.enableBeacon || !navigator.sendBeacon) return;
|
|
|
|
const summary = generateSummary();
|
|
const blob = new Blob([JSON.stringify(summary)], { type: 'application/json' });
|
|
|
|
try {
|
|
navigator.sendBeacon(CONFIG.beaconEndpoint, blob);
|
|
} catch (e) {
|
|
console.warn('Failed to send performance metrics:', e);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Initialize performance monitoring
|
|
*/
|
|
function init() {
|
|
// Start observers early
|
|
observeLCP();
|
|
observeFID();
|
|
observeCLS();
|
|
observeLongTasks();
|
|
|
|
// Collect metrics after page load
|
|
window.addEventListener('load', () => {
|
|
// Delay to ensure all metrics are available
|
|
setTimeout(() => {
|
|
collectNavigationTiming();
|
|
collectPaintTiming();
|
|
collectResourceTiming();
|
|
logMetrics();
|
|
sendMetrics();
|
|
}, CONFIG.reportDelay);
|
|
});
|
|
|
|
// Start memory monitoring
|
|
monitorMemory();
|
|
|
|
// Report on page unload
|
|
window.addEventListener('visibilitychange', () => {
|
|
if (document.visibilityState === 'hidden') {
|
|
sendMetrics();
|
|
}
|
|
});
|
|
}
|
|
|
|
// Expose API
|
|
window.ThrillWikiPerformance = {
|
|
getMetrics: () => metrics,
|
|
getSummary: generateSummary,
|
|
logMetrics: logMetrics,
|
|
};
|
|
|
|
// Initialize
|
|
if (document.readyState === 'loading') {
|
|
document.addEventListener('DOMContentLoaded', init);
|
|
} else {
|
|
init();
|
|
}
|
|
})();
|