mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-25 02:31:09 -05:00
Add secret management guide, client-side performance monitoring, and search accessibility enhancements
- 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.
This commit is contained in:
411
static/js/performance-monitor.js
Normal file
411
static/js/performance-monitor.js
Normal file
@@ -0,0 +1,411 @@
|
||||
/**
|
||||
* 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();
|
||||
}
|
||||
})();
|
||||
Reference in New Issue
Block a user