RC2
This commit is contained in:
64
.gitignore
vendored
64
.gitignore
vendored
@ -1,25 +1,65 @@
|
||||
# Bytecode-Dateien
|
||||
# Bytecode files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
|
||||
# Virtuelle Umgebungen
|
||||
# Virtual environments
|
||||
.venv/
|
||||
venv/
|
||||
|
||||
# Betriebssystem-Dateien
|
||||
# System files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Logfiles und Dumps
|
||||
# Logs and temporary files
|
||||
*.log
|
||||
*.bak
|
||||
*.swp
|
||||
*.tmp
|
||||
|
||||
# IDEs und Editoren
|
||||
# Node.js dependencies
|
||||
node_modules/
|
||||
package.json
|
||||
package-lock.json
|
||||
yarn.lock
|
||||
|
||||
# Development documentation
|
||||
PERFORMANCE-TESTING.md
|
||||
|
||||
# Build and distribution
|
||||
dist/
|
||||
build/
|
||||
*.min.js
|
||||
*.min.css
|
||||
*.map
|
||||
|
||||
# Testing
|
||||
coverage/
|
||||
*.test.js
|
||||
*.spec.js
|
||||
.nyc_output/
|
||||
|
||||
# Environment variables
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
|
||||
# Debug logs
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# IDEs and editors
|
||||
.vscode/
|
||||
.idea/
|
||||
*.sublime-workspace
|
||||
*.sublime-project
|
||||
|
||||
# Local development
|
||||
.cache/
|
||||
.temp/
|
||||
.tmp/
|
||||
|
||||
# Project specific
|
||||
data/*
|
||||
!data/.gitignore
|
||||
|
||||
@ -28,3 +68,17 @@ log/*
|
||||
|
||||
streams/*
|
||||
!streams/.gitignore
|
||||
|
||||
# Test files
|
||||
tests/**/*.js
|
||||
!tests/*.test.js
|
||||
!tests/*.spec.js
|
||||
!tests/README.md
|
||||
!tests/profile-auth.js
|
||||
|
||||
# Performance test results
|
||||
performance-results/*
|
||||
!performance-results/.gitkeep
|
||||
|
||||
# Legacy files
|
||||
public_streams.txt
|
||||
|
@ -1,2 +0,0 @@
|
||||
{"uid":"oibchello","size":3371119,"mtime":1752994076}
|
||||
{"uid":"orangeicebear","size":1734396,"mtime":1748767975}
|
179
run-navigation-test.js
Normal file
179
run-navigation-test.js
Normal file
@ -0,0 +1,179 @@
|
||||
const puppeteer = require('puppeteer');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
// Configuration
|
||||
const BASE_URL = 'http://localhost:8000'; // Update this if your app runs on a different URL
|
||||
const TEST_ITERATIONS = 5;
|
||||
const OUTPUT_DIR = path.join(__dirname, 'performance-results');
|
||||
const TIMESTAMP = new Date().toISOString().replace(/[:.]/g, '-');
|
||||
|
||||
// Ensure output directory exists
|
||||
if (!fs.existsSync(OUTPUT_DIR)) {
|
||||
fs.mkdirSync(OUTPUT_DIR, { recursive: true });
|
||||
}
|
||||
|
||||
// Helper function to save results
|
||||
function saveResults(data, filename) {
|
||||
const filepath = path.join(OUTPUT_DIR, `${filename}-${TIMESTAMP}.json`);
|
||||
fs.writeFileSync(filepath, JSON.stringify(data, null, 2));
|
||||
console.log(`Results saved to ${filepath}`);
|
||||
return filepath;
|
||||
}
|
||||
|
||||
// Test runner
|
||||
async function runNavigationTest() {
|
||||
const browser = await puppeteer.launch({
|
||||
headless: false, // Set to true for CI/CD
|
||||
devtools: true,
|
||||
args: [
|
||||
'--no-sandbox',
|
||||
'--disable-setuid-sandbox',
|
||||
'--disable-dev-shm-usage',
|
||||
'--disable-accelerated-2d-canvas',
|
||||
'--no-first-run',
|
||||
'--no-zygote',
|
||||
'--single-process',
|
||||
'--disable-gpu',
|
||||
'--js-flags="--max-old-space-size=1024"'
|
||||
]
|
||||
});
|
||||
|
||||
try {
|
||||
const page = await browser.newPage();
|
||||
|
||||
// Enable performance metrics
|
||||
await page.setViewport({ width: 1280, height: 800 });
|
||||
await page.setDefaultNavigationTimeout(60000);
|
||||
|
||||
// Set up console logging
|
||||
page.on('console', msg => console.log('PAGE LOG:', msg.text()));
|
||||
|
||||
// Load the performance test script
|
||||
const testScript = fs.readFileSync(path.join(__dirname, 'static', 'router-perf-test.js'), 'utf8');
|
||||
|
||||
// Test guest mode
|
||||
console.log('Testing guest mode...');
|
||||
await page.goto(`${BASE_URL}`, { waitUntil: 'networkidle0' });
|
||||
|
||||
// Inject and run the test
|
||||
const guestResults = await page.evaluate(async (script) => {
|
||||
// Inject the test script
|
||||
const scriptEl = document.createElement('script');
|
||||
scriptEl.textContent = script;
|
||||
document.head.appendChild(scriptEl);
|
||||
|
||||
// Run the test
|
||||
const test = new RouterPerfTest();
|
||||
return await test.runTest('guest');
|
||||
}, testScript);
|
||||
|
||||
saveResults(guestResults, 'guest-results');
|
||||
|
||||
// Test logged-in mode (if credentials are provided)
|
||||
if (process.env.LOGIN_EMAIL && process.env.LOGIN_PASSWORD) {
|
||||
console.log('Testing logged-in mode...');
|
||||
|
||||
// Navigate to the test page with authentication token
|
||||
console.log('Authenticating with provided token...');
|
||||
await page.goto('https://dicta2stream.net/?token=d96561d7-6c95-4e10-80f7-62d5d3a5bd04', {
|
||||
waitUntil: 'networkidle0',
|
||||
timeout: 60000
|
||||
});
|
||||
|
||||
// Wait for authentication to complete and verify
|
||||
try {
|
||||
await page.waitForSelector('body.authenticated', {
|
||||
timeout: 30000,
|
||||
visible: true
|
||||
});
|
||||
console.log('✅ Successfully authenticated');
|
||||
|
||||
// Verify user is actually logged in
|
||||
const isAuthenticated = await page.evaluate(() => {
|
||||
return document.body.classList.contains('authenticated');
|
||||
});
|
||||
|
||||
if (!isAuthenticated) {
|
||||
throw new Error('Authentication failed - not in authenticated state');
|
||||
}
|
||||
|
||||
// Force a navigation to ensure the state is stable
|
||||
await page.goto('https://dicta2stream.net/#welcome-page', {
|
||||
waitUntil: 'networkidle0',
|
||||
timeout: 30000
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Authentication failed:', error.message);
|
||||
// Take a screenshot for debugging
|
||||
await page.screenshot({ path: 'auth-failure.png' });
|
||||
console.log('Screenshot saved as auth-failure.png');
|
||||
throw error;
|
||||
}
|
||||
|
||||
// Wait for the page to fully load after login
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
// Run the test in logged-in mode
|
||||
const loggedInResults = await page.evaluate(async (script) => {
|
||||
const test = new RouterPerfTest();
|
||||
return await test.runTest('loggedIn');
|
||||
}, testScript);
|
||||
|
||||
saveResults(loggedInResults, 'loggedin-results');
|
||||
|
||||
// Generate comparison report
|
||||
const comparison = {
|
||||
timestamp: new Date().toISOString(),
|
||||
guest: {
|
||||
avg: guestResults.overall.avg,
|
||||
min: guestResults.overall.min,
|
||||
max: guestResults.overall.max
|
||||
},
|
||||
loggedIn: {
|
||||
avg: loggedInResults.overall.avg,
|
||||
min: loggedInResults.overall.min,
|
||||
max: loggedInResults.overall.max
|
||||
},
|
||||
difference: {
|
||||
ms: loggedInResults.overall.avg - guestResults.overall.avg,
|
||||
percent: ((loggedInResults.overall.avg - guestResults.overall.avg) / guestResults.overall.avg) * 100
|
||||
}
|
||||
};
|
||||
|
||||
const reportPath = saveResults(comparison, 'performance-comparison');
|
||||
console.log(`\nPerformance comparison report generated at: ${reportPath}`);
|
||||
|
||||
// Take a screenshot of the results
|
||||
await page.screenshot({ path: path.join(OUTPUT_DIR, `results-${TIMESTAMP}.png`), fullPage: true });
|
||||
|
||||
return comparison;
|
||||
}
|
||||
|
||||
return guestResults;
|
||||
|
||||
} catch (error) {
|
||||
console.error('Test failed:', error);
|
||||
// Take a screenshot on error
|
||||
if (page) {
|
||||
await page.screenshot({ path: path.join(OUTPUT_DIR, `error-${TIMESTAMP}.png`), fullPage: true });
|
||||
}
|
||||
throw error;
|
||||
|
||||
} finally {
|
||||
await browser.close();
|
||||
}
|
||||
}
|
||||
|
||||
// Run the test
|
||||
runNavigationTest()
|
||||
.then(results => {
|
||||
console.log('Test completed successfully');
|
||||
console.log('Results:', JSON.stringify(results, null, 2));
|
||||
process.exit(0);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Test failed:', error);
|
||||
process.exit(1);
|
||||
});
|
@ -505,10 +505,15 @@ window.clearTimeout = (id) => {
|
||||
originalClearTimeout(id);
|
||||
};
|
||||
|
||||
// Track auth check calls
|
||||
// Track auth check calls and cache state
|
||||
let lastAuthCheckTime = 0;
|
||||
let authCheckCounter = 0;
|
||||
const AUTH_CHECK_DEBOUNCE = 1000; // 1 second
|
||||
let authStateCache = {
|
||||
timestamp: 0,
|
||||
value: null,
|
||||
ttl: 5000 // Cache TTL in milliseconds
|
||||
};
|
||||
|
||||
// Override console.log to capture all logs
|
||||
const originalConsoleLog = console.log;
|
||||
@ -822,33 +827,46 @@ function updateAccountDeletionVisibility(isAuthenticated) {
|
||||
});
|
||||
}
|
||||
|
||||
// Check authentication state and update UI
|
||||
function checkAuthState() {
|
||||
// Debounce rapid calls
|
||||
// Check authentication state and update UI with caching and debouncing
|
||||
function checkAuthState(force = false) {
|
||||
const now = Date.now();
|
||||
if (now - lastAuthCheckTime < AUTH_CHECK_DEBOUNCE) {
|
||||
|
||||
// Return cached value if still valid and not forcing a refresh
|
||||
if (!force && now - authStateCache.timestamp < authStateCache.ttl && authStateCache.value !== null) {
|
||||
return authStateCache.value;
|
||||
}
|
||||
|
||||
// Debounce rapid calls
|
||||
if (now - lastAuthCheckTime < AUTH_CHECK_DEBOUNCE && !force) {
|
||||
return wasAuthenticated === true;
|
||||
}
|
||||
|
||||
lastAuthCheckTime = now;
|
||||
authCheckCounter++;
|
||||
|
||||
// Check various authentication indicators
|
||||
const hasAuthCookie = document.cookie.includes('isAuthenticated=true');
|
||||
const hasUidCookie = document.cookie.includes('uid=');
|
||||
const hasLocalStorageAuth = localStorage.getItem('isAuthenticated') === 'true';
|
||||
const hasAuthToken = !!localStorage.getItem('authToken');
|
||||
// Use a single check for authentication state
|
||||
let isAuthenticated = false;
|
||||
|
||||
// User is considered authenticated if any of these are true
|
||||
const isAuthenticated = hasAuthCookie || hasUidCookie || hasLocalStorageAuth || hasAuthToken;
|
||||
// Check the most likely indicators first for better performance
|
||||
isAuthenticated =
|
||||
document.cookie.includes('isAuthenticated=') ||
|
||||
document.cookie.includes('uid=') ||
|
||||
localStorage.getItem('isAuthenticated') === 'true' ||
|
||||
!!localStorage.getItem('authToken');
|
||||
|
||||
if (DEBUG_AUTH_STATE || isAuthenticated !== wasAuthenticated) {
|
||||
// Update cache
|
||||
authStateCache = {
|
||||
timestamp: now,
|
||||
value: isAuthenticated,
|
||||
ttl: isAuthenticated ? 30000 : 5000 // Longer TTL for authenticated users
|
||||
};
|
||||
|
||||
if (DEBUG_AUTH_STATE && isAuthenticated !== wasAuthenticated) {
|
||||
console.log('Auth State Check:', {
|
||||
hasAuthCookie,
|
||||
hasUidCookie,
|
||||
hasLocalStorageAuth,
|
||||
hasAuthToken,
|
||||
isAuthenticated,
|
||||
wasAuthenticated
|
||||
wasAuthenticated,
|
||||
cacheHit: !force && now - authStateCache.timestamp < authStateCache.ttl,
|
||||
cacheAge: now - authStateCache.timestamp
|
||||
});
|
||||
}
|
||||
|
||||
@ -887,19 +905,44 @@ function checkAuthState() {
|
||||
return isAuthenticated;
|
||||
}
|
||||
|
||||
// Periodically check authentication state
|
||||
// Periodically check authentication state with optimized polling
|
||||
function setupAuthStatePolling() {
|
||||
// Initial check
|
||||
checkAuthState();
|
||||
// Initial check with force to ensure we get the latest state
|
||||
checkAuthState(true);
|
||||
|
||||
// Check every 30 seconds instead of 2 to reduce load
|
||||
setInterval(checkAuthState, 30000);
|
||||
// Use a single interval for all checks
|
||||
const checkAndUpdate = () => {
|
||||
// Only force check if the page is visible
|
||||
checkAuthState(!document.hidden);
|
||||
};
|
||||
|
||||
// Also check after certain events that might affect auth state
|
||||
window.addEventListener('storage', checkAuthState);
|
||||
document.addEventListener('visibilitychange', () => {
|
||||
if (!document.hidden) checkAuthState();
|
||||
});
|
||||
// Check every 30 seconds (reduced from previous implementation)
|
||||
const AUTH_CHECK_INTERVAL = 30000;
|
||||
setInterval(checkAndUpdate, AUTH_CHECK_INTERVAL);
|
||||
|
||||
// Listen for storage events (like login/logout from other tabs)
|
||||
const handleStorageEvent = (e) => {
|
||||
if (['isAuthenticated', 'authToken', 'uid'].includes(e.key)) {
|
||||
checkAuthState(true); // Force check on relevant storage changes
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('storage', handleStorageEvent);
|
||||
|
||||
// Check auth state when page becomes visible
|
||||
const handleVisibilityChange = () => {
|
||||
if (!document.hidden) {
|
||||
checkAuthState(true);
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('visibilitychange', handleVisibilityChange);
|
||||
|
||||
// Cleanup function
|
||||
return () => {
|
||||
window.removeEventListener('storage', handleStorageEvent);
|
||||
document.removeEventListener('visibilitychange', handleVisibilityChange);
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
|
@ -1,50 +1,69 @@
|
||||
// Force hide guest navigation for authenticated users
|
||||
function fixMobileNavigation() {
|
||||
console.log('[FIX-NAV] Running navigation fix...');
|
||||
// Debounce helper function
|
||||
function debounce(func, wait) {
|
||||
let timeout;
|
||||
return function() {
|
||||
const context = this;
|
||||
const args = arguments;
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(() => func.apply(context, args), wait);
|
||||
};
|
||||
}
|
||||
|
||||
// Check if user is authenticated
|
||||
const hasAuthCookie = document.cookie.includes('isAuthenticated=true');
|
||||
const hasUidCookie = document.cookie.includes('uid=');
|
||||
const hasLocalStorageAuth = localStorage.getItem('isAuthenticated') === 'true';
|
||||
const hasAuthToken = localStorage.getItem('authToken') !== null;
|
||||
const isAuthenticated = hasAuthCookie || hasUidCookie || hasLocalStorageAuth || hasAuthToken;
|
||||
// Throttle helper function
|
||||
function throttle(func, limit) {
|
||||
let inThrottle;
|
||||
return function() {
|
||||
const args = arguments;
|
||||
const context = this;
|
||||
if (!inThrottle) {
|
||||
func.apply(context, args);
|
||||
inThrottle = true;
|
||||
setTimeout(() => inThrottle = false, limit);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
console.log('[FIX-NAV] Authentication state:', {
|
||||
isAuthenticated,
|
||||
hasAuthCookie,
|
||||
hasUidCookie,
|
||||
hasLocalStorageAuth,
|
||||
hasAuthToken
|
||||
});
|
||||
// Check authentication state once and cache it
|
||||
function getAuthState() {
|
||||
return (
|
||||
document.cookie.includes('isAuthenticated=') ||
|
||||
document.cookie.includes('uid=') ||
|
||||
localStorage.getItem('isAuthenticated') === 'true' ||
|
||||
!!localStorage.getItem('authToken')
|
||||
);
|
||||
}
|
||||
|
||||
// Update navigation based on authentication state
|
||||
function updateNavigation() {
|
||||
const isAuthenticated = getAuthState();
|
||||
|
||||
// Only proceed if the authentication state has changed
|
||||
if (isAuthenticated === updateNavigation.lastState) {
|
||||
return;
|
||||
}
|
||||
updateNavigation.lastState = isAuthenticated;
|
||||
|
||||
if (isAuthenticated) {
|
||||
// Force hide guest navigation with !important styles
|
||||
// Hide guest navigation for authenticated users
|
||||
const guestNav = document.getElementById('guest-dashboard');
|
||||
if (guestNav) {
|
||||
console.log('[FIX-NAV] Hiding guest navigation');
|
||||
guestNav.style.cssText = `
|
||||
display: none !important;
|
||||
visibility: hidden !important;
|
||||
opacity: 0 !important;
|
||||
height: 0 !important;
|
||||
width: 0 !important;
|
||||
padding: 0 !important;
|
||||
margin: 0 !important;
|
||||
border: none !important;
|
||||
position: absolute !important;
|
||||
overflow: hidden !important;
|
||||
position: absolute !important;
|
||||
clip: rect(0, 0, 0, 0) !important;
|
||||
pointer-events: none !important;
|
||||
`;
|
||||
guestNav.classList.add('force-hidden');
|
||||
}
|
||||
|
||||
// Ensure user navigation is visible with !important styles
|
||||
// Show user navigation if it exists
|
||||
const userNav = document.getElementById('user-dashboard');
|
||||
if (userNav) {
|
||||
console.log('[FIX-NAV] Showing user navigation');
|
||||
userNav.style.cssText = `
|
||||
display: flex !important;
|
||||
display: block !important;
|
||||
visibility: visible !important;
|
||||
opacity: 1 !important;
|
||||
height: auto !important;
|
||||
@ -55,25 +74,9 @@ function fixMobileNavigation() {
|
||||
userNav.classList.add('force-visible');
|
||||
}
|
||||
|
||||
// Add authenticated class to body
|
||||
// Update body classes
|
||||
document.body.classList.add('authenticated');
|
||||
document.body.classList.remove('guest-mode');
|
||||
|
||||
// Prevent default behavior of nav links that might cause page reloads
|
||||
document.querySelectorAll('a[href^="#"]').forEach(link => {
|
||||
link.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
const targetId = link.getAttribute('href');
|
||||
if (targetId && targetId !== '#') {
|
||||
// Use history API to update URL without full page reload
|
||||
history.pushState(null, '', targetId);
|
||||
// Dispatch a custom event that other scripts can listen for
|
||||
window.dispatchEvent(new CustomEvent('hashchange'));
|
||||
// Force re-apply our navigation fix
|
||||
setTimeout(fixMobileNavigation, 0);
|
||||
}
|
||||
});
|
||||
});
|
||||
} else {
|
||||
// User is not authenticated - ensure guest nav is visible
|
||||
const guestNav = document.getElementById('guest-dashboard');
|
||||
@ -85,50 +88,53 @@ function fixMobileNavigation() {
|
||||
}
|
||||
}
|
||||
|
||||
// Run on page load
|
||||
document.addEventListener('DOMContentLoaded', fixMobileNavigation);
|
||||
// Initialize the navigation state
|
||||
updateNavigation.lastState = null;
|
||||
|
||||
// Also run after a short delay to catch any dynamic content
|
||||
setTimeout(fixMobileNavigation, 100);
|
||||
setTimeout(fixMobileNavigation, 300);
|
||||
setTimeout(fixMobileNavigation, 1000);
|
||||
// Handle navigation link clicks
|
||||
function handleNavLinkClick(e) {
|
||||
const link = e.target.closest('a[href^="#"]');
|
||||
if (!link) return;
|
||||
|
||||
// Listen for hash changes (navigation)
|
||||
window.addEventListener('hashchange', fixMobileNavigation);
|
||||
|
||||
// Listen for pushState/replaceState (SPA navigation)
|
||||
const originalPushState = history.pushState;
|
||||
const originalReplaceState = history.replaceState;
|
||||
|
||||
history.pushState = function() {
|
||||
originalPushState.apply(this, arguments);
|
||||
setTimeout(fixMobileNavigation, 0);
|
||||
};
|
||||
|
||||
history.replaceState = function() {
|
||||
originalReplaceState.apply(this, arguments);
|
||||
setTimeout(fixMobileNavigation, 0);
|
||||
};
|
||||
|
||||
// Run on any DOM mutations (for dynamically loaded content)
|
||||
const observer = new MutationObserver((mutations) => {
|
||||
let shouldFix = false;
|
||||
mutations.forEach((mutation) => {
|
||||
if (mutation.addedNodes.length || mutation.removedNodes.length) {
|
||||
shouldFix = true;
|
||||
}
|
||||
});
|
||||
if (shouldFix) {
|
||||
setTimeout(fixMobileNavigation, 0);
|
||||
e.preventDefault();
|
||||
const targetId = link.getAttribute('href');
|
||||
if (targetId && targetId !== '#') {
|
||||
// Update URL without triggering full page reload
|
||||
history.pushState(null, '', targetId);
|
||||
// Dispatch a custom event for other scripts
|
||||
window.dispatchEvent(new CustomEvent('hashchange'));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
observer.observe(document.body, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
attributes: true,
|
||||
attributeFilter: ['class', 'style', 'id']
|
||||
});
|
||||
// Initialize the navigation system
|
||||
function initNavigation() {
|
||||
// Set up event delegation for navigation links
|
||||
document.body.addEventListener('click', handleNavLinkClick);
|
||||
|
||||
// Export for debugging
|
||||
window.fixMobileNavigation = fixMobileNavigation;
|
||||
// Listen for hash changes (throttled)
|
||||
window.addEventListener('hashchange', throttle(updateNavigation, 100));
|
||||
|
||||
// Listen for storage changes (like login/logout from other tabs)
|
||||
window.addEventListener('storage', debounce(updateNavigation, 100));
|
||||
|
||||
// Check for authentication changes periodically (every 30 seconds)
|
||||
setInterval(updateNavigation, 30000);
|
||||
|
||||
// Initial update
|
||||
updateNavigation();
|
||||
}
|
||||
|
||||
// Run initialization when DOM is ready
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', initNavigation);
|
||||
} else {
|
||||
// DOMContentLoaded has already fired
|
||||
initNavigation();
|
||||
}
|
||||
|
||||
// Export for testing if needed
|
||||
window.navigationUtils = {
|
||||
updateNavigation,
|
||||
getAuthState,
|
||||
initNavigation
|
||||
};
|
||||
|
@ -8,5 +8,7 @@
|
||||
<a href="#" data-target="privacy-page">Privacy</a>
|
||||
<span class="separator">•</span>
|
||||
<a href="#" data-target="imprint-page">Imprint</a>
|
||||
<span class="separator auth-only" style="display: none;">•</span>
|
||||
<a href="#" data-target="your-stream" class="auth-only" style="display: none;">Your Stream</a>
|
||||
</div>
|
||||
</footer>
|
||||
|
@ -258,8 +258,15 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
e.preventDefault();
|
||||
const target = link.dataset.target;
|
||||
if (target) {
|
||||
// Show the target section without updating URL hash
|
||||
showSection(target);
|
||||
// Update URL hash to maintain proper history state
|
||||
window.location.hash = target;
|
||||
// Use the router to handle the navigation
|
||||
if (router && typeof router.showOnly === 'function') {
|
||||
router.showOnly(target);
|
||||
} else {
|
||||
// Fallback to showSection if router is not available
|
||||
showSection(target);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
374
tests/profile-auth.js
Normal file
374
tests/profile-auth.js
Normal file
@ -0,0 +1,374 @@
|
||||
/**
|
||||
* Authentication Profiling Tool for dicta2stream
|
||||
*
|
||||
* This script profiles the authentication-related operations during navigation
|
||||
* to identify performance bottlenecks in the logged-in experience.
|
||||
*
|
||||
* Usage:
|
||||
* 1. Open browser console (F12 → Console)
|
||||
* 2. Copy and paste this entire script
|
||||
* 3. Run: profileAuthNavigation()
|
||||
*/
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
// Store original methods we want to profile
|
||||
const originalFetch = window.fetch;
|
||||
const originalXHROpen = XMLHttpRequest.prototype.open;
|
||||
const originalXHRSend = XMLHttpRequest.prototype.send;
|
||||
|
||||
// Track authentication-related operations with detailed metrics
|
||||
const authProfile = {
|
||||
// Core metrics
|
||||
startTime: null,
|
||||
operations: [],
|
||||
navigationEvents: [],
|
||||
|
||||
// Counters
|
||||
fetchCount: 0,
|
||||
xhrCount: 0,
|
||||
domUpdates: 0,
|
||||
authChecks: 0,
|
||||
|
||||
// Performance metrics
|
||||
totalTime: 0,
|
||||
maxFrameTime: 0,
|
||||
longTasks: [],
|
||||
|
||||
// Memory tracking
|
||||
memorySamples: [],
|
||||
maxMemory: 0,
|
||||
|
||||
// Navigation tracking
|
||||
currentNavigation: null,
|
||||
navigationStart: null
|
||||
};
|
||||
|
||||
// Track long tasks and performance metrics
|
||||
if (window.PerformanceObserver) {
|
||||
// Check which entry types are supported
|
||||
const supportedEntryTypes = PerformanceObserver.supportedEntryTypes || [];
|
||||
|
||||
// Only set up observers for supported types
|
||||
const perfObserver = new PerformanceObserver((list) => {
|
||||
const entries = list.getEntries();
|
||||
for (const entry of entries) {
|
||||
// Track any task longer than 50ms as a long task
|
||||
if (entry.duration > 50) {
|
||||
authProfile.longTasks.push({
|
||||
startTime: entry.startTime,
|
||||
duration: entry.duration,
|
||||
name: entry.name || 'unknown',
|
||||
type: entry.entryType
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Try to observe supported entry types
|
||||
try {
|
||||
// Check for longtask support (not available in all browsers)
|
||||
if (supportedEntryTypes.includes('longtask')) {
|
||||
perfObserver.observe({ entryTypes: ['longtask'] });
|
||||
}
|
||||
|
||||
// Always try to observe paint timing
|
||||
try {
|
||||
if (supportedEntryTypes.includes('paint')) {
|
||||
perfObserver.observe({ entryTypes: ['paint'] });
|
||||
} else {
|
||||
// Fallback to buffered paint observation
|
||||
perfObserver.observe({ type: 'paint', buffered: true });
|
||||
}
|
||||
} catch (e) {
|
||||
console.debug('Paint timing not supported:', e.message);
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
console.debug('Performance observation not supported:', e.message);
|
||||
}
|
||||
}
|
||||
|
||||
// Instrument fetch API
|
||||
window.fetch = async function(...args) {
|
||||
const url = typeof args[0] === 'string' ? args[0] : args[0].url;
|
||||
const isAuthRelated = isAuthOperation(url);
|
||||
|
||||
if (isAuthRelated) {
|
||||
const start = performance.now();
|
||||
authProfile.fetchCount++;
|
||||
|
||||
try {
|
||||
const response = await originalFetch.apply(this, args);
|
||||
const duration = performance.now() - start;
|
||||
|
||||
authProfile.operations.push({
|
||||
type: 'fetch',
|
||||
url,
|
||||
duration,
|
||||
timestamp: new Date().toISOString(),
|
||||
status: response.status,
|
||||
size: response.headers.get('content-length') || 'unknown'
|
||||
});
|
||||
|
||||
return response;
|
||||
} catch (error) {
|
||||
const duration = performance.now() - start;
|
||||
authProfile.operations.push({
|
||||
type: 'fetch',
|
||||
url,
|
||||
duration,
|
||||
timestamp: new Date().toISOString(),
|
||||
error: error.message,
|
||||
status: 'error'
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
return originalFetch.apply(this, args);
|
||||
};
|
||||
|
||||
// Instrument XHR
|
||||
XMLHttpRequest.prototype.open = function(method, url) {
|
||||
this._authProfile = isAuthOperation(url);
|
||||
if (this._authProfile) {
|
||||
this._startTime = performance.now();
|
||||
this._url = url;
|
||||
authProfile.xhrCount++;
|
||||
}
|
||||
return originalXHROpen.apply(this, arguments);
|
||||
};
|
||||
|
||||
XMLHttpRequest.prototype.send = function(body) {
|
||||
if (this._authProfile) {
|
||||
this.addEventListener('load', () => {
|
||||
const duration = performance.now() - this._startTime;
|
||||
authProfile.operations.push({
|
||||
type: 'xhr',
|
||||
url: this._url,
|
||||
duration,
|
||||
timestamp: new Date().toISOString(),
|
||||
status: this.status,
|
||||
size: this.getResponseHeader('content-length') || 'unknown'
|
||||
});
|
||||
});
|
||||
|
||||
this.addEventListener('error', (error) => {
|
||||
const duration = performance.now() - this._startTime;
|
||||
authProfile.operations.push({
|
||||
type: 'xhr',
|
||||
url: this._url,
|
||||
duration,
|
||||
timestamp: new Date().toISOString(),
|
||||
error: error.message,
|
||||
status: 'error'
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return originalXHRSend.apply(this, arguments);
|
||||
};
|
||||
|
||||
// Track DOM updates after navigation with more details
|
||||
const observer = new MutationObserver((mutations) => {
|
||||
if (document.body.classList.contains('authenticated')) {
|
||||
const now = performance.now();
|
||||
const updateInfo = {
|
||||
timestamp: now,
|
||||
mutations: mutations.length,
|
||||
addedNodes: 0,
|
||||
removedNodes: 0,
|
||||
attributeChanges: 0,
|
||||
characterDataChanges: 0
|
||||
};
|
||||
|
||||
mutations.forEach(mutation => {
|
||||
updateInfo.addedNodes += mutation.addedNodes.length || 0;
|
||||
updateInfo.removedNodes += mutation.removedNodes.length || 0;
|
||||
updateInfo.attributeChanges += mutation.type === 'attributes' ? 1 : 0;
|
||||
updateInfo.characterDataChanges += mutation.type === 'characterData' ? 1 : 0;
|
||||
});
|
||||
|
||||
authProfile.domUpdates += mutations.length;
|
||||
|
||||
// Track memory usage if supported
|
||||
if (window.performance && window.performance.memory) {
|
||||
updateInfo.memoryUsed = performance.memory.usedJSHeapSize;
|
||||
updateInfo.memoryTotal = performance.memory.totalJSHeapSize;
|
||||
updateInfo.memoryLimit = performance.memory.jsHeapSizeLimit;
|
||||
|
||||
authProfile.memorySamples.push({
|
||||
timestamp: now,
|
||||
memory: performance.memory.usedJSHeapSize
|
||||
});
|
||||
|
||||
authProfile.maxMemory = Math.max(
|
||||
authProfile.maxMemory,
|
||||
performance.memory.usedJSHeapSize
|
||||
);
|
||||
}
|
||||
|
||||
// Track frame time
|
||||
requestAnimationFrame(() => {
|
||||
const frameTime = performance.now() - now;
|
||||
authProfile.maxFrameTime = Math.max(authProfile.maxFrameTime, frameTime);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Track authentication state changes
|
||||
const originalAddClass = DOMTokenList.prototype.add;
|
||||
const originalRemoveClass = DOMTokenList.prototype.remove;
|
||||
|
||||
DOMTokenList.prototype.add = function(...args) {
|
||||
if (this === document.body.classList && args[0] === 'authenticated') {
|
||||
authProfile.authChecks++;
|
||||
if (!authProfile.startTime) {
|
||||
authProfile.startTime = performance.now();
|
||||
observer.observe(document.body, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
attributes: true,
|
||||
characterData: true
|
||||
});
|
||||
}
|
||||
}
|
||||
return originalAddClass.apply(this, args);
|
||||
};
|
||||
|
||||
DOMTokenList.prototype.remove = function(...args) {
|
||||
if (this === document.body.classList && args[0] === 'authenticated') {
|
||||
authProfile.totalTime = performance.now() - (authProfile.startTime || performance.now());
|
||||
observer.disconnect();
|
||||
}
|
||||
return originalRemoveClass.apply(this, args);
|
||||
};
|
||||
|
||||
// Helper to identify auth-related operations
|
||||
function isAuthOperation(url) {
|
||||
if (!url) return false;
|
||||
const authKeywords = [
|
||||
'auth', 'login', 'session', 'token', 'user', 'profile',
|
||||
'me', 'account', 'verify', 'validate', 'check'
|
||||
];
|
||||
return authKeywords.some(keyword =>
|
||||
url.toLowerCase().includes(keyword)
|
||||
);
|
||||
}
|
||||
|
||||
// Main function to run the profile
|
||||
window.profileAuthNavigation = async function() {
|
||||
// Reset profile with all metrics
|
||||
Object.assign(authProfile, {
|
||||
startTime: null,
|
||||
operations: [],
|
||||
navigationEvents: [],
|
||||
fetchCount: 0,
|
||||
xhrCount: 0,
|
||||
domUpdates: 0,
|
||||
authChecks: 0,
|
||||
totalTime: 0,
|
||||
maxFrameTime: 0,
|
||||
longTasks: [],
|
||||
memorySamples: [],
|
||||
maxMemory: 0,
|
||||
currentNavigation: null,
|
||||
navigationStart: null
|
||||
});
|
||||
|
||||
// Track navigation events
|
||||
if (window.performance) {
|
||||
const navObserver = new PerformanceObserver((list) => {
|
||||
list.getEntries().forEach(entry => {
|
||||
if (entry.entryType === 'navigation') {
|
||||
authProfile.navigationEvents.push({
|
||||
type: 'navigation',
|
||||
name: entry.name,
|
||||
startTime: entry.startTime,
|
||||
duration: entry.duration,
|
||||
domComplete: entry.domComplete,
|
||||
domContentLoaded: entry.domContentLoadedEventEnd,
|
||||
load: entry.loadEventEnd
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
try {
|
||||
navObserver.observe({ entryTypes: ['navigation'] });
|
||||
} catch (e) {
|
||||
console.warn('Navigation timing not supported:', e);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Starting authentication navigation profile...');
|
||||
console.log('1. Navigate to a few pages while logged in');
|
||||
console.log('2. Run getAuthProfile() to see the results');
|
||||
console.log('3. Run resetAuthProfile() to start over');
|
||||
|
||||
// Add global accessor
|
||||
window.getAuthProfile = function() {
|
||||
const now = performance.now();
|
||||
const duration = authProfile.startTime ? (now - authProfile.startTime) / 1000 : 0;
|
||||
|
||||
console.log('%c\n=== AUTHENTICATION PROFILE RESULTS ===', 'font-weight:bold;font-size:1.2em');
|
||||
|
||||
// Summary
|
||||
console.log('\n%c--- PERFORMANCE SUMMARY ---', 'font-weight:bold');
|
||||
console.log(`Total Monitoring Time: ${duration.toFixed(2)}s`);
|
||||
console.log(`Authentication Checks: ${authProfile.authChecks}`);
|
||||
console.log(`Fetch API Calls: ${authProfile.fetchCount}`);
|
||||
console.log(`XHR Requests: ${authProfile.xhrCount}`);
|
||||
console.log(`DOM Updates: ${authProfile.domUpdates}`);
|
||||
console.log(`Max Frame Time: ${authProfile.maxFrameTime.toFixed(2)}ms`);
|
||||
|
||||
// Memory usage
|
||||
if (authProfile.memorySamples.length > 0) {
|
||||
const lastMem = authProfile.memorySamples[authProfile.memorySamples.length - 1];
|
||||
console.log(`\n%c--- MEMORY USAGE ---`, 'font-weight:bold');
|
||||
console.log(`Max Memory Used: ${(authProfile.maxMemory / 1024 / 1024).toFixed(2)} MB`);
|
||||
console.log(`Current Memory: ${(lastMem.memory / 1024 / 1024).toFixed(2)} MB`);
|
||||
}
|
||||
|
||||
// Long tasks
|
||||
if (authProfile.longTasks.length > 0) {
|
||||
console.log(`\n%c--- LONG TASKS (${authProfile.longTasks.length} > 50ms) ---`, 'font-weight:bold');
|
||||
authProfile.longTasks
|
||||
.sort((a, b) => b.duration - a.duration)
|
||||
.slice(0, 5)
|
||||
.forEach((task, i) => {
|
||||
console.log(`#${i + 1} ${task.name}: ${task.duration.toFixed(2)}ms`);
|
||||
});
|
||||
}
|
||||
|
||||
// Slow operations
|
||||
if (authProfile.operations.length > 0) {
|
||||
console.log('\n%c--- SLOW OPERATIONS ---', 'font-weight:bold');
|
||||
const sortedOps = [...authProfile.operations].sort((a, b) => b.duration - a.duration);
|
||||
sortedOps.slice(0, 10).forEach((op, i) => {
|
||||
const memUsage = op.memory ? ` | +${(op.memory / 1024).toFixed(2)}KB` : '';
|
||||
console.log(`#${i + 1} [${op.type.toUpperCase()}] ${op.url || 'unknown'}: ${op.duration.toFixed(2)}ms${memUsage}`);
|
||||
});
|
||||
}
|
||||
|
||||
return authProfile;
|
||||
};
|
||||
|
||||
window.resetAuthProfile = function() {
|
||||
Object.assign(authProfile, {
|
||||
startTime: null,
|
||||
operations: [],
|
||||
fetchCount: 0,
|
||||
xhrCount: 0,
|
||||
domUpdates: 0,
|
||||
authChecks: 0,
|
||||
totalTime: 0
|
||||
});
|
||||
console.log('Authentication profile has been reset');
|
||||
};
|
||||
};
|
||||
|
||||
console.log('Authentication profiler loaded. Run profileAuthNavigation() to start.');
|
||||
})();
|
Reference in New Issue
Block a user