feat: improve logging

This commit is contained in:
Rim 2025-04-01 16:29:29 -04:00
parent e16a0a85ee
commit 0df8728515
2 changed files with 276 additions and 30 deletions

48
app.js
View File

@ -899,6 +899,8 @@ app.post("/api/search", async (req, res) => {
app.post('/api/log', (req, res) => {
const clientIP = req.headers['x-forwarded-for'] || req.ip || req.connection.remoteAddress;
const userAgent = req.headers['user-agent'];
const referer = req.headers['referer'];
const origin = req.headers['origin'];
let logData;
try {
@ -912,11 +914,25 @@ app.post('/api/log', (req, res) => {
logData = { eventType: 'unknown', timestamp: new Date().toISOString() };
}
// Log the data
console.log(`[USER ACTIVITY] ${new Date().toISOString()} | IP: ${clientIP} | Type: ${logData.eventType} | ${JSON.stringify({
// Enrich log with server-side data
const enrichedLog = {
...logData,
userAgent
})}`);
meta: {
clientIP,
userAgent,
referer,
origin,
requestHeaders: sanitizeHeaders(req.headers),
serverTimestamp: new Date().toISOString(),
requestId: req.id || Math.random().toString(36).substring(2, 15)
}
};
// For structured logging in production, consider using a logging service
console.log(`[USER_ACTIVITY] ${JSON.stringify(enrichedLog)}`);
// Optional: Store logs in database for advanced analytics
// storeLogInDatabase(enrichedLog);
} catch (error) {
console.error('Error processing log data:', error);
@ -927,6 +943,30 @@ app.post('/api/log', (req, res) => {
res.status(200).send();
});
// Helper function to remove sensitive data from headers
function sanitizeHeaders(headers) {
const safeHeaders = { ...headers };
// Remove potential sensitive information
const sensitiveHeaders = ['authorization', 'cookie', 'set-cookie'];
sensitiveHeaders.forEach(header => {
if (safeHeaders[header]) {
safeHeaders[header] = '[REDACTED]';
}
});
return safeHeaders;
}
// Database storage function
/*
function storeLogInDatabase(logData) {
// Example with MongoDB
db.collection('user_logs').insertOne(logData)
.catch(err => console.error('Failed to store log in database:', err));
}
*/
// Basic health check endpoint
app.get("/health", (req, res) => {
res.json({ status: "ok", timestamp: new Date().toISOString() });

View File

@ -593,14 +593,150 @@ document.addEventListener('DOMContentLoaded', () => {
let lastActivity = Date.now();
let activityTimer;
// Log initial session start
logEvent('session_start', { sessionId });
// Store user's device and viewport info
const deviceInfo = {
screenWidth: window.screen.width,
screenHeight: window.screen.height,
viewportWidth: window.innerWidth,
viewportHeight: window.innerHeight,
pixelRatio: window.devicePixelRatio,
userAgent: navigator.userAgent
};
// Log initial session start with device info
logEvent('session_start', {
sessionId,
deviceInfo,
referrer: document.referrer,
landingPage: window.location.pathname
});
// Update last activity time on user interactions
['click', 'mousemove', 'keypress', 'scroll', 'touchstart'].forEach(event => {
document.addEventListener(event, () => lastActivity = Date.now());
});
// Track all clicks with detailed element info
document.addEventListener('click', (e) => {
const target = e.target;
// Build an object with element details
const elementInfo = {
tagName: target.tagName,
id: target.id || undefined,
className: target.className || undefined,
text: target.innerText ? target.innerText.substring(0, 100) : undefined,
href: target.href || target.closest('a')?.href,
value: target.type !== 'password' ? target.value : undefined,
type: target.type || undefined,
name: target.name || undefined,
coordinates: {
x: e.clientX,
y: e.clientY,
pageX: e.pageX,
pageY: e.pageY
},
dataAttributes: getDataAttributes(target)
};
logEvent('element_click', {
sessionId,
elementInfo,
path: window.location.pathname
});
});
// Helper function to extract data attributes
function getDataAttributes(element) {
if (!element.dataset) return {};
return Object.entries(element.dataset)
.reduce((acc, [key, value]) => {
acc[key] = value;
return acc;
}, {});
}
// Track tab visibility changes
document.addEventListener('visibilitychange', () => {
logEvent('visibility_change', {
sessionId,
visibilityState: document.visibilityState,
timestamp: Date.now()
});
});
// Track navigation between tabs/elements with focus events
document.addEventListener('focusin', (e) => {
const target = e.target;
if (target.tagName) {
logEvent('focus_change', {
sessionId,
element: {
tagName: target.tagName,
id: target.id || undefined,
className: target.className || undefined,
name: target.name || undefined,
type: target.type || undefined
}
});
}
});
// Track scroll events with throttling (every 500ms max)
let lastScrollLog = 0;
document.addEventListener('scroll', () => {
const now = Date.now();
if (now - lastScrollLog > 500) {
lastScrollLog = now;
logEvent('scroll', {
sessionId,
scrollPosition: {
x: window.scrollX,
y: window.scrollY
},
viewportHeight: window.innerHeight,
documentHeight: document.documentElement.scrollHeight,
percentScrolled: Math.round((window.scrollY / (document.documentElement.scrollHeight - window.innerHeight)) * 100)
});
}
});
// Track window resize events with throttling
let resizeTimer;
window.addEventListener('resize', () => {
clearTimeout(resizeTimer);
resizeTimer = setTimeout(() => {
logEvent('window_resize', {
sessionId,
dimensions: {
width: window.innerWidth,
height: window.innerHeight
}
});
}, 500);
});
// Monitor form interactions
document.addEventListener('change', (e) => {
const target = e.target;
// Don't log passwords
if (target.type === 'password') return;
if (target.tagName === 'SELECT' || target.tagName === 'INPUT' || target.tagName === 'TEXTAREA') {
logEvent('form_change', {
sessionId,
element: {
tagName: target.tagName,
id: target.id || undefined,
name: target.name || undefined,
type: target.type || undefined,
value: ['checkbox', 'radio'].includes(target.type) ? target.checked : target.value
}
});
}
});
// Activity check every 60 seconds
activityTimer = setInterval(() => {
const inactiveTime = Date.now() - lastActivity;
@ -619,18 +755,86 @@ document.addEventListener('DOMContentLoaded', () => {
const duration = Math.round((Date.now() - sessionStart) / 1000);
logEvent('session_end', {
sessionId,
duration
duration,
path: window.location.pathname
});
});
// Track pushState and replaceState for SPA navigation
const originalPushState = history.pushState;
const originalReplaceState = history.replaceState;
history.pushState = function() {
originalPushState.apply(this, arguments);
handleHistoryChange();
};
history.replaceState = function() {
originalReplaceState.apply(this, arguments);
handleHistoryChange();
};
function handleHistoryChange() {
logEvent('page_view', {
sessionId,
path: window.location.pathname,
query: window.location.search,
title: document.title
});
}
// Track network requests (AJAX calls)
const originalFetch = window.fetch;
window.fetch = function() {
const startTime = Date.now();
const url = arguments[0];
const method = arguments[1]?.method || 'GET';
// Only log the URL and method, not the payload
logEvent('network_request_start', {
sessionId,
url: typeof url === 'string' ? url : url.url,
method
});
return originalFetch.apply(this, arguments)
.then(response => {
logEvent('network_request_complete', {
sessionId,
url: typeof url === 'string' ? url : url.url,
method,
status: response.status,
duration: Date.now() - startTime
});
return response;
})
.catch(error => {
logEvent('network_request_error', {
sessionId,
url: typeof url === 'string' ? url : url.url,
method,
error: error.message,
duration: Date.now() - startTime
});
throw error;
});
};
// Function to send logs to server
function logEvent(eventType, data) {
// Add current page info to all events
const enrichedData = {
...data,
url: window.location.href,
userTimestamp: Date.now()
};
// Use sendBeacon for reliability during page unload
if (navigator.sendBeacon) {
const blob = new Blob([JSON.stringify({
eventType,
timestamp: new Date().toISOString(),
...data
...enrichedData
})], { type: 'application/json' });
navigator.sendBeacon('/api/log', blob);
@ -643,43 +847,45 @@ document.addEventListener('DOMContentLoaded', () => {
body: JSON.stringify({
eventType,
timestamp: new Date().toISOString(),
...data
...enrichedData
})
}).catch(e => console.error('Logging error:', e));
}
}
// Track page navigation within the SPA (if applicable)
window.addEventListener('popstate', () => {
logEvent('page_view', {
sessionId,
path: window.location.pathname
});
});
// Optional: Track specific user actions
// Example: Track when a player search is performed
// Track specific user actions from your existing code
document.querySelectorAll('form').forEach(form => {
form.addEventListener('submit', (e) => {
const formData = new FormData(form);
let actionData = {};
// Collect non-sensitive form data
if (formData.get('username')) {
actionData.username = formData.get('username');
}
if (formData.get('platform')) {
actionData.platform = formData.get('platform');
}
if (formData.get('game')) {
actionData.game = formData.get('game');
for (const [key, value] of formData.entries()) {
// Skip passwords
if (key.toLowerCase().includes('password')) continue;
actionData[key] = value;
}
logEvent('user_action', {
logEvent('form_submit', {
sessionId,
action: 'search',
formId: form.id || undefined,
formName: form.getAttribute('name') || undefined,
formAction: form.action || undefined,
formMethod: form.method || 'get',
...actionData
});
});
});
});
// Track errors
window.addEventListener('error', (e) => {
logEvent('js_error', {
sessionId,
message: e.message,
source: e.filename,
lineno: e.lineno,
colno: e.colno,
stack: e.error?.stack
});
});
});