diff --git a/app.js b/app.js index 64685b3..e062eab 100644 --- a/app.js +++ b/app.js @@ -2,6 +2,7 @@ const express = require("express"); const path = require("path"); const bodyParser = require("body-parser"); const API = require("./src/js/index.js"); +const { logger } = require('./src/js/logger'); const favicon = require('serve-favicon'); const app = express(); const port = process.env.PORT || 3512; @@ -30,12 +31,12 @@ try { if (fs.existsSync(replacementsPath)) { const replacementsContent = fs.readFileSync(replacementsPath, 'utf8'); keyReplacements = JSON.parse(replacementsContent); - // console.log("Replacements loaded successfully"); + // logger.debug("Replacements loaded successfully"); } else { - console.log("replacements.json not found, key replacement disabled"); + logger.warn("replacements.json not found, key replacement disabled"); } } catch (error) { - console.error("Error loading replacements file:", error); + logger.error("Error loading replacements file:", { error: error.message }); } const replaceJsonKeys = (obj) => { @@ -52,14 +53,14 @@ const replaceJsonKeys = (obj) => { // DEBUG: Log replacements when they happen // if (newKey !== key) { - // console.log(`Replacing key "${key}" with "${newKey}"`); + // logger.debug(`Replacing key "${key}" with "${newKey}"`); // } // Also check if the value should be replaced (if it's a string) let value = obj[key]; if (typeof value === 'string' && keyReplacements[value]) { value = keyReplacements[value]; - // console.log(`Replacing value "${obj[key]}" with "${value}"`); + // logger.debug(`Replacing value "${obj[key]}" with "${value}"`); } // Process value recursively if it's an object or array @@ -140,26 +141,26 @@ const timeoutPromise = (ms) => { // Helper function to ensure login const ensureLogin = async (ssoToken) => { if (!activeSessions.has(ssoToken)) { - console.log(`Attempting to login with SSO token: ${ssoToken.substring(0, 5)}...`); - // console.log(`Attempting to login with SSO token: ${ssoToken}`); + logger.info(`Attempting to login with SSO token: ${ssoToken.substring(0, 5)}...`); + // logger.info(`Attempting to login with SSO token: ${ssoToken}`); const loginResult = await Promise.race([ API.login(ssoToken), timeoutPromise(10000), // 10 second timeout ]); - console.log(`Login successful: ${JSON.stringify(loginResult)}`); - console.log(`Session created at: ${new Date().toISOString()}`); + logger.debug(`Login successful: ${JSON.stringify(loginResult)}`); + logger.debug(`Session created at: ${new Date().toISOString()}`); activeSessions.set(ssoToken, new Date()); } else { - console.log("Using existing session"); + logger.debug("Using existing session"); } }; // Helper function to handle API errors const handleApiError = (error, res) => { - console.error("API Error:", error); - console.error(`Error Stack: ${error.stack}`); - console.error(`Error Time: ${new Date().toISOString()}`); + logger.error("API Error:", error); + logger.error(`Error Stack: ${error.stack}`); + logger.error(`Error Time: ${new Date().toISOString()}`); // Try to extract more useful information from the error let errorMessage = error.message || "Unknown API error"; @@ -167,7 +168,7 @@ const handleApiError = (error, res) => { // Handle the specific JSON parsing error if (errorName === "SyntaxError" && errorMessage.includes("JSON")) { - console.log("JSON parsing error detected"); + logger.debug("JSON parsing error detected"); return res.status(200).json({ status: "error", message: @@ -188,9 +189,9 @@ const handleApiError = (error, res) => { // API endpoint to fetch stats app.post("/api/stats", async (req, res) => { - console.log("Received request for /api/stats"); - console.log(`Request IP: ${req.headers['x-forwarded-for'] || req.ip || req.connection.remoteAddress}`); - console.log(`Request JSON: ${JSON.stringify({ + logger.debug("Received request for /api/stats"); + logger.debug(`Request IP: ${req.headers['x-forwarded-for'] || req.ip || req.connection.remoteAddress}`); + logger.debug(`Request JSON: ${JSON.stringify({ username: req.body.username, platform: req.body.platform, game: req.body.game, @@ -203,17 +204,17 @@ app.post("/api/stats", async (req, res) => { const { username, ssoToken, platform, game, apiCall, sanitize, replaceKeys } = req.body; /* - console.log( + logger.debug( `Request details - Username: ${username}, Platform: ${platform}, Game: ${game}, API Call: ${apiCall}` ); - console.log("=== STATS REQUEST ==="); - console.log(`Username: ${username || 'Not provided'}`); - console.log(`Platform: ${platform}`); - console.log(`Game: ${game}`); - console.log(`API Call: ${apiCall}`); - console.log(`Processing Options - Sanitize: ${sanitize}, Replace Keys: ${replaceKeys}`); - console.log("====================="); */ + logger.debug("=== STATS REQUEST ==="); + logger.debug(`Username: ${username || 'Not provided'}`); + logger.debug(`Platform: ${platform}`); + logger.debug(`Game: ${game}`); + logger.debug(`API Call: ${apiCall}`); + logger.debug(`Processing Options - Sanitize: ${sanitize}, Replace Keys: ${replaceKeys}`); + logger.debug("====================="); */ if (!ssoToken) { return res.status(400).json({ error: "SSO Token is required" }); @@ -226,7 +227,7 @@ app.post("/api/stats", async (req, res) => { // Clear previous session if it exists if (activeSessions.has(ssoToken)) { - console.log("Clearing previous session"); + logger.debug("Clearing previous session"); activeSessions.delete(ssoToken); } @@ -255,7 +256,7 @@ app.post("/api/stats", async (req, res) => { // Check if the platform is valid for the game const requiresUno = ["mw2", "wz2", "mw3", "wzm"].includes(game); if (requiresUno && platform !== "uno" && apiCall !== "mapList") { - console.log(`${game} requires Uno ID`); + logger.warn(`${game} requires Uno ID`); return res.status(200).json({ status: "error", message: `${game} requires Uno ID (numerical ID)`, @@ -264,7 +265,7 @@ app.post("/api/stats", async (req, res) => { } try { - console.log( + logger.debug( `Attempting to fetch ${game} data for ${username} on ${platform}` ); let data; @@ -384,13 +385,13 @@ app.post("/api/stats", async (req, res) => { } } - console.log("Data fetched successfully"); - console.log(`Response Size: ~${JSON.stringify(data).length / 1024} KB`); - console.log(`Response Time: ${new Date().toISOString()}`); + logger.debug("Data fetched successfully"); + logger.debug(`Response Size: ~${JSON.stringify(data).length / 1024} KB`); + logger.debug(`Response Time: ${new Date().toISOString()}`); // Safely handle the response data if (!data) { - console.log("No data returned from API"); + logger.warn("No data returned from API"); return res.json({ status: "partial_success", message: "No data returned from API, but no error thrown", @@ -399,7 +400,7 @@ app.post("/api/stats", async (req, res) => { }); } - // console.log("Returning data to client"); + // logger.debug("Returning data to client"); const { sanitize, replaceKeys } = req.body; @@ -426,9 +427,9 @@ app.post("/api/stats", async (req, res) => { // API endpoint to fetch recent matches app.post("/api/matches", async (req, res) => { - console.log("Received request for /api/matches"); - console.log(`Request IP: ${req.headers['x-forwarded-for'] || req.ip || req.connection.remoteAddress}`); - console.log(`Request JSON: ${JSON.stringify({ + logger.debug("Received request for /api/matches"); + logger.debug(`Request IP: ${req.headers['x-forwarded-for'] || req.ip || req.connection.remoteAddress}`); + logger.debug(`Request JSON: ${JSON.stringify({ username: req.body.username, platform: req.body.platform, game: req.body.game, @@ -440,16 +441,16 @@ app.post("/api/matches", async (req, res) => { const { username, ssoToken, platform, game, sanitize, replaceKeys } = req.body; /* - console.log( + logger.debug( `Request details - Username: ${username}, Platform: ${platform}, Game: ${game}` ); - console.log("=== MATCHES REQUEST ==="); - console.log(`Username: ${username}`); - console.log(`Platform: ${platform}`); - console.log(`Game: ${game}`); - console.log(`Processing Options - Sanitize: ${sanitize}, Replace Keys: ${replaceKeys}`); - console.log("========================"); */ + logger.debug("=== MATCHES REQUEST ==="); + logger.debug(`Username: ${username}`); + logger.debug(`Platform: ${platform}`); + logger.debug(`Game: ${game}`); + logger.debug(`Processing Options - Sanitize: ${sanitize}, Replace Keys: ${replaceKeys}`); + logger.debug("========================"); */ if (!username || !ssoToken) { return res @@ -478,7 +479,7 @@ app.post("/api/matches", async (req, res) => { }; try { - console.log( + logger.debug( `Attempting to fetch combat history for ${username} on ${platform}` ); let data; @@ -565,9 +566,9 @@ app.post("/api/matches", async (req, res) => { // API endpoint to fetch match info app.post("/api/matchInfo", async (req, res) => { - console.log("Received request for /api/matchInfo"); - console.log(`Request IP: ${req.headers['x-forwarded-for'] || req.ip || req.connection.remoteAddress}`); - console.log(`Request JSON: ${JSON.stringify({ + logger.debug("Received request for /api/matchInfo"); + logger.debug(`Request IP: ${req.headers['x-forwarded-for'] || req.ip || req.connection.remoteAddress}`); + logger.debug(`Request JSON: ${JSON.stringify({ matchId: req.body.matchId, platform: req.body.platform, game: req.body.game, @@ -580,16 +581,16 @@ app.post("/api/matchInfo", async (req, res) => { const mode = "mp"; /* - console.log( + logger.debug( `Request details - Match ID: ${matchId}, Platform: ${platform}, Game: ${game}` ); - console.log("=== MATCH INFO REQUEST ==="); - console.log(`Match ID: ${matchId}`); - console.log(`Platform: ${platform}`); - console.log(`Game: ${game}`); - console.log(`Processing Options - Sanitize: ${sanitize}, Replace Keys: ${replaceKeys}`); - console.log("=========================="); */ + logger.debug("=== MATCH INFO REQUEST ==="); + logger.debug(`Match ID: ${matchId}`); + logger.debug(`Platform: ${platform}`); + logger.debug(`Game: ${game}`); + logger.debug(`Processing Options - Sanitize: ${sanitize}, Replace Keys: ${replaceKeys}`); + logger.debug("=========================="); */ if (!matchId || !ssoToken) { return res @@ -618,7 +619,7 @@ app.post("/api/matchInfo", async (req, res) => { }; try { - console.log(`Attempting to fetch match info for match ID: ${matchId}`); + logger.debug(`Attempting to fetch match info for match ID: ${matchId}`); let data; // Fetch match info based on game @@ -691,9 +692,9 @@ app.post("/api/matchInfo", async (req, res) => { // API endpoint for user-related API calls app.post("/api/user", async (req, res) => { - console.log("Received request for /api/user"); - console.log(`Request IP: ${req.headers['x-forwarded-for'] || req.ip || req.connection.remoteAddress}`); - console.log(`Request JSON: ${JSON.stringify({ + logger.debug("Received request for /api/user"); + logger.debug(`Request IP: ${req.headers['x-forwarded-for'] || req.ip || req.connection.remoteAddress}`); + logger.debug(`Request JSON: ${JSON.stringify({ username: req.body.username, platform: req.body.platform, userCall: req.body.userCall, @@ -705,16 +706,16 @@ app.post("/api/user", async (req, res) => { const { username, ssoToken, platform, userCall, sanitize, replaceKeys } = req.body; /* - console.log( + logger.debug( `Request details - Username: ${username}, Platform: ${platform}, User Call: ${userCall}` ); - console.log("=== USER DATA REQUEST ==="); - console.log(`Username: ${username || 'Not provided'}`); - console.log(`Platform: ${platform}`); - console.log(`User Call: ${userCall}`); - console.log(`Processing Options - Sanitize: ${sanitize}, Replace Keys: ${replaceKeys}`); - console.log("========================="); */ + logger.debug("=== USER DATA REQUEST ==="); + logger.debug(`Username: ${username || 'Not provided'}`); + logger.debug(`Platform: ${platform}`); + logger.debug(`User Call: ${userCall}`); + logger.debug(`Processing Options - Sanitize: ${sanitize}, Replace Keys: ${replaceKeys}`); + logger.debug("========================="); */ if (!ssoToken) { return res.status(400).json({ error: "SSO Token is required" }); @@ -753,7 +754,7 @@ app.post("/api/user", async (req, res) => { }; try { - console.log(`Attempting to fetch user data for ${userCall}`); + logger.debug(`Attempting to fetch user data for ${userCall}`); let data; // Fetch user data based on userCall @@ -817,9 +818,9 @@ app.post("/api/user", async (req, res) => { // API endpoint for fuzzy search app.post("/api/search", async (req, res) => { - console.log("Received request for /api/search"); - console.log(`Request IP: ${req.headers['x-forwarded-for'] || req.ip || req.connection.remoteAddress}`); - console.log(`Request JSON: ${JSON.stringify({ + logger.debug("Received request for /api/search"); + logger.debug(`Request IP: ${req.headers['x-forwarded-for'] || req.ip || req.connection.remoteAddress}`); + logger.debug(`Request JSON: ${JSON.stringify({ username: req.body.username, platform: req.body.platform, sanitize: req.body.sanitize, @@ -830,15 +831,15 @@ app.post("/api/search", async (req, res) => { const { username, ssoToken, platform, sanitize, replaceKeys } = req.body; /* - console.log( + logger.debug( `Request details - Username to search: ${username}, Platform: ${platform}` ); - console.log("=== SEARCH REQUEST ==="); - console.log(`Search Term: ${username}`); - console.log(`Platform: ${platform}`); - console.log(`Processing Options - Sanitize: ${sanitize}, Replace Keys: ${replaceKeys}`); - console.log("======================"); */ + logger.debug("=== SEARCH REQUEST ==="); + logger.debug(`Search Term: ${username}`); + logger.debug(`Platform: ${platform}`); + logger.debug(`Processing Options - Sanitize: ${sanitize}, Replace Keys: ${replaceKeys}`); + logger.debug("======================"); */ if (!username || !ssoToken) { return res @@ -867,7 +868,7 @@ app.post("/api/search", async (req, res) => { }; try { - console.log( + logger.debug( `Attempting fuzzy search for ${username} on platform ${platform}` ); const data = await fetchWithTimeout(() => @@ -929,15 +930,14 @@ app.post('/api/log', (req, res) => { } }; - // 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); + // Use the dedicated user activity logger + logger.userActivity(enrichedLog.eventType || 'unknown', enrichedLog); } catch (error) { - console.error('Error processing log data:', error); - console.log('Raw request body:', req.body); + logger.error('Error processing log data', { + error: error.message, + rawBody: typeof req.body === 'object' ? '[Object]' : req.body + }); } // Always return 200 to avoid client-side errors @@ -980,5 +980,5 @@ app.get("/", (req, res) => { // Start the server app.listen(port, () => { - console.log(`Server running on http://localhost:${port}`); + logger.info(`Server running on http://localhost:${port}`); }); diff --git a/src/js/backend.js b/src/js/backend.js index d252000..1ea7e05 100644 --- a/src/js/backend.js +++ b/src/js/backend.js @@ -223,308 +223,3 @@ function processTimestamps(data, timezone, keysToConvert = ['date', 'dateAdded', return result; } - -// Initialize session tracking when the page loads -document.addEventListener('DOMContentLoaded', () => { - // Generate a unique session ID - const sessionId = Date.now().toString(36) + Math.random().toString(36).substr(2); - const sessionStart = Date.now(); - let lastActivity = Date.now(); - let activityTimer; - - // 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; - // Log if user has been inactive for more than 3 minutes - if (inactiveTime > 180000) { - logEvent('user_inactive', { - sessionId, - inactiveTime: Math.round(inactiveTime / 1000) - }); - } - }, 60000); - - // Log session end when page is closed - window.addEventListener('beforeunload', () => { - clearInterval(activityTimer); - const duration = Math.round((Date.now() - sessionStart) / 1000); - logEvent('session_end', { - sessionId, - 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(), - ...enrichedData - })], { type: 'application/json' }); - - navigator.sendBeacon('/api/log', blob); - } else { - // Fallback for browsers that don't support sendBeacon - fetch('/api/log', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - keepalive: true, - body: JSON.stringify({ - eventType, - timestamp: new Date().toISOString(), - ...enrichedData - }) - }).catch(e => console.error('Logging error:', e)); - } - } - - // 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 - for (const [key, value] of formData.entries()) { - // Skip passwords - if (key.toLowerCase().includes('password')) continue; - actionData[key] = value; - } - - logEvent('form_submit', { - sessionId, - 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 - }); - }); -}); diff --git a/src/js/frontend.js b/src/js/frontend.js index 85707ed..ff47c40 100644 --- a/src/js/frontend.js +++ b/src/js/frontend.js @@ -3,6 +3,55 @@ window.uiAPI = { displayError }; +// Configure client-side logging settings +const clientLogger = { + // Control how much we log to reduce spam + settings: { + enableDetailedNetworkLogs: false, // Set to false to reduce network logging + logScrollEvents: false, // Disable scroll event logging + logMouseMovements: false, // Disable mouse movement logging + logFocusEvents: false, // Disable focus event logging + logResizeEvents: false, // Disable resize event logging + minimumInactivityTime: 300000, // Only log inactivity after 5 minutes (300000ms) + clickDebounceTime: 1000, // Debounce click logging to once per second + }, + // Throttle function to limit how often a function can run + throttle(func, limit) { + let lastRun; + return function(...args) { + if (!lastRun || Date.now() - lastRun >= limit) { + lastRun = Date.now(); + func.apply(this, args); + } + }; + }, + // Logging function with filtering + log(eventType, data) { + // Use sendBeacon for reliability during page unload + if (navigator.sendBeacon) { + const blob = new Blob([JSON.stringify({ + eventType, + timestamp: new Date().toISOString(), + ...data + })], { type: 'application/json' }); + + navigator.sendBeacon('/api/log', blob); + } else { + // Fallback for browsers that don't support sendBeacon + fetch('/api/log', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + keepalive: true, + body: JSON.stringify({ + eventType, + timestamp: new Date().toISOString(), + ...data + }) + }).catch(e => console.error('Logging error:', e)); + } + } + }; + // Initialize once DOM is loaded document.addEventListener("DOMContentLoaded", function() { initTabSwitching(); @@ -12,6 +61,7 @@ document.addEventListener("DOMContentLoaded", function() { setupProcessingOptions(); setupTimeOptions(); addSyncListeners(); + initializeSessionTracking(); }); // Tab switching logic @@ -378,3 +428,238 @@ function addSyncListeners() { // Add change listeners for platform sync document.getElementById("platform").addEventListener("change", syncUsernames); } + +// Initialize session tracking when the page loads +function initializeSessionTracking() { + // Generate a unique session ID + const sessionId = Date.now().toString(36) + Math.random().toString(36).substr(2); + const sessionStart = Date.now(); + let lastActivity = Date.now(); + let activityTimer; + + // 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 + clientLogger.log('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 clicks with throttling to reduce spam + let lastClickTime = 0; + document.addEventListener('click', (e) => { + const now = Date.now(); + if (now - lastClickTime < clientLogger.settings.clickDebounceTime) { + return; // Skip if clicked too recently + } + lastClickTime = now; + + 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 + }; + + clientLogger.log('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', () => { + clientLogger.log('visibility_change', { + sessionId, + visibilityState: document.visibilityState, + timestamp: Date.now() + }); + }); + + // Track navigation between tabs/elements with focus events (if enabled) + if (clientLogger.settings.logFocusEvents) { + document.addEventListener('focusin', (e) => { + const target = e.target; + if (target.tagName) { + clientLogger.log('focus_change', { + sessionId, + element: { + tagName: target.tagName, + id: target.id || undefined, + className: target.className || undefined + } + }); + } + }); + } + + // Track scroll events with throttling (if enabled) + if (clientLogger.settings.logScrollEvents) { + document.addEventListener('scroll', + clientLogger.throttle(() => { + clientLogger.log('scroll', { + sessionId, + scrollPosition: { + y: window.scrollY + }, + percentScrolled: Math.round((window.scrollY / (document.documentElement.scrollHeight - window.innerHeight)) * 100) + }); + }, 1000) // Log at most once per second + ); + } + + // Track window resize events with throttling (if enabled) + if (clientLogger.settings.logResizeEvents) { + window.addEventListener('resize', + clientLogger.throttle(() => { + clientLogger.log('window_resize', { + sessionId, + dimensions: { + width: window.innerWidth, + height: window.innerHeight + } + }); + }, 1000) // Log at most once per second + ); + } + + // Monitor form interactions - only for submits and important changes + document.addEventListener('submit', (e) => { + const form = e.target; + clientLogger.log('form_submit', { + sessionId, + formId: form.id || undefined, + formName: form.getAttribute('name') || undefined + }); + }); + + // Activity check every 60 seconds + activityTimer = setInterval(() => { + const inactiveTime = Date.now() - lastActivity; + // Log if user has been inactive for more than the minimum threshold + if (inactiveTime > clientLogger.settings.minimumInactivityTime) { + clientLogger.log('user_inactive', { + sessionId, + inactiveTime: Math.round(inactiveTime / 1000) + }); + } + }, 60000); + + // Log session end when page is closed + window.addEventListener('beforeunload', () => { + clearInterval(activityTimer); + const duration = Math.round((Date.now() - sessionStart) / 1000); + clientLogger.log('session_end', { + sessionId, + 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() { + clientLogger.log('page_view', { + sessionId, + path: window.location.pathname, + query: window.location.search, + title: document.title + }); + } + + // Track network requests only if detailed network logging is enabled + if (clientLogger.settings.enableDetailedNetworkLogs) { + const originalFetch = window.fetch; + window.fetch = function() { + const startTime = Date.now(); + const url = arguments[0]; + const method = arguments[1]?.method || 'GET'; + + // Skip logging for log endpoint to avoid infinite loops + if (typeof url === 'string' && url.includes('/api/log')) { + return originalFetch.apply(this, arguments); + } + + // Only log the URL and method, not the payload + clientLogger.log('network_request_start', { + sessionId, + url: typeof url === 'string' ? url : url.url, + method + }); + + return originalFetch.apply(this, arguments) + .then(response => { + clientLogger.log('network_request_complete', { + sessionId, + url: typeof url === 'string' ? url : url.url, + method, + status: response.status, + duration: Date.now() - startTime + }); + return response; + }) + .catch(error => { + clientLogger.log('network_request_error', { + sessionId, + url: typeof url === 'string' ? url : url.url, + method, + error: error.message, + duration: Date.now() - startTime + }); + throw error; + }); + }; + } + + // Track errors + window.addEventListener('error', (e) => { + clientLogger.log('js_error', { + sessionId, + message: e.message, + source: e.filename, + lineno: e.lineno, + colno: e.colno + }); + }); + } \ No newline at end of file diff --git a/src/js/logger.js b/src/js/logger.js new file mode 100644 index 0000000..009e46c --- /dev/null +++ b/src/js/logger.js @@ -0,0 +1,139 @@ +// logger.js +const fs = require('fs'); +const path = require('path'); + +class Logger { + constructor(options = {}) { + this.options = { + logToConsole: options.logToConsole !== false, + logToFile: options.logToFile || false, + logDirectory: options.logDirectory || path.join(__dirname, '..', '..', 'logs'), + userActivityLogFile: options.userActivityLogFile || 'user-activity.log', + apiLogFile: options.apiLogFile || 'api.log', + // Log levels: debug, info, warn, error + minLevel: options.minLevel || 'info' + }; + + // Create log directory if it doesn't exist and logging to file is enabled + if (this.options.logToFile) { + if (!fs.existsSync(this.options.logDirectory)) { + fs.mkdirSync(this.options.logDirectory, { recursive: true }); + } + } + + // Log levels and their priorities + this.levels = { + debug: 0, + info: 1, + warn: 2, + error: 3 + }; + } + + shouldLog(level) { + return this.levels[level] >= this.levels[this.options.minLevel]; + } + + formatLogEntry(type, message, data = {}) { + const timestamp = new Date().toISOString(); + const logObject = { + timestamp, + type, + message + }; + + if (Object.keys(data).length > 0) { + logObject.data = data; + } + + return JSON.stringify(logObject); + } + + writeToFile(content, isUserActivity = false) { + if (!this.options.logToFile) return; + + const logFile = isUserActivity + ? path.join(this.options.logDirectory, this.options.userActivityLogFile) + : path.join(this.options.logDirectory, this.options.apiLogFile); + + fs.appendFile(logFile, content + '\n', (err) => { + if (err) { + console.error(`Error writing to log file: ${err.message}`); + } + }); + } + + debug(message, data = {}) { + if (!this.shouldLog('debug')) return; + + const logEntry = this.formatLogEntry('DEBUG', message, data); + + if (this.options.logToConsole) { + console.debug(logEntry); + } + + this.writeToFile(logEntry); + } + + info(message, data = {}) { + if (!this.shouldLog('info')) return; + + const logEntry = this.formatLogEntry('INFO', message, data); + + if (this.options.logToConsole) { + console.log(logEntry); + } + + this.writeToFile(logEntry); + } + + warn(message, data = {}) { + if (!this.shouldLog('warn')) return; + + const logEntry = this.formatLogEntry('WARN', message, data); + + if (this.options.logToConsole) { + console.warn(logEntry); + } + + this.writeToFile(logEntry); + } + + error(message, data = {}) { + if (!this.shouldLog('error')) return; + + const logEntry = this.formatLogEntry('ERROR', message, data); + + if (this.options.logToConsole) { + console.error(logEntry); + } + + this.writeToFile(logEntry); + } + + // Specialized method for user activity logging + userActivity(eventType, data = {}) { + const logEntry = this.formatLogEntry('USER_ACTIVITY', eventType, data); + + // Don't log user activity to the console + /* + if (this.options.logToConsole) { + console.log(`[USER_ACTIVITY] ${logEntry}`); + } + */ + + this.writeToFile(logEntry, true); + } +} + +// Create and export default logger instance +const defaultLogger = new Logger({ + logToConsole: true, + logToFile: true, + minLevel: process.env.NODE_ENV === 'production' ? 'info' : 'debug' +}); + +module.exports = { + Logger, + logger: defaultLogger +}; \ No newline at end of file