let tutorialDismissed = false; let currentData = null; let outputFormat = "json"; // Initialize once DOM is loaded document.addEventListener("DOMContentLoaded", function() { initTabSwitching(); addEnterKeyListeners(); setupDownloadButton(); setupFormatSelector(); setupProcessingOptions(); setupTimeOptions(); addSyncListeners(); }); // Tab switching logic function initTabSwitching() { document.querySelectorAll(".tab").forEach((tab) => { tab.addEventListener("click", () => { document .querySelectorAll(".tab") .forEach((t) => t.classList.remove("active")); document .querySelectorAll(".tab-content") .forEach((c) => c.classList.remove("active")); tab.classList.add("active"); const tabId = tab.getAttribute("data-tab"); document.getElementById(`${tabId}-tab`).classList.add("active"); }); }); } // Setup processing options (sanitize/replace) function setupProcessingOptions() { document.getElementById("sanitizeOption").addEventListener("change", function() { if (currentData) { // Re-fetch with new options const activeTab = document.querySelector(".tab.active").getAttribute("data-tab"); triggerActiveTabButton(); } }); document.getElementById("replaceKeysOption").addEventListener("change", function() { if (currentData) { // Re-fetch with new options const activeTab = document.querySelector(".tab.active").getAttribute("data-tab"); triggerActiveTabButton(); } }); } // Setup format selector function setupFormatSelector() { document.getElementById("outputFormat").addEventListener("change", function() { outputFormat = this.value; if (currentData) { displayResults(currentData); } }); } // Fetch stats document.getElementById("fetchStats").addEventListener("click", async () => { const username = document.getElementById("username").value.trim(); const ssoToken = document.getElementById("ssoToken").value.trim(); const platform = document.getElementById("platform").value; const game = document.getElementById("game").value; const apiCall = document.getElementById("apiCall").value; const sanitize = document.getElementById("sanitizeOption").checked; const replaceKeys = document.getElementById("replaceKeysOption").checked; await fetchData("/api/stats", { username, ssoToken, platform, game, apiCall, sanitize, replaceKeys }); }); // Fetch match history document.getElementById("fetchMatches").addEventListener("click", async () => { const username = document.getElementById("matchUsername").value.trim(); const ssoToken = document.getElementById("ssoToken").value.trim(); const platform = document.getElementById("matchPlatform").value; const game = document.getElementById("matchGame").value; const sanitize = document.getElementById("sanitizeOption").checked; const replaceKeys = document.getElementById("replaceKeysOption").checked; await fetchData("/api/matches", { username, ssoToken, platform, game, sanitize, replaceKeys }); }); // Fetch match details document.getElementById("fetchMatchInfo").addEventListener("click", async () => { const matchId = document.getElementById("matchId").value.trim(); const ssoToken = document.getElementById("ssoToken").value.trim(); const platform = document.getElementById("matchPlatform").value; const game = document.getElementById("matchGame").value; const sanitize = document.getElementById("sanitizeOption").checked; const replaceKeys = document.getElementById("replaceKeysOption").checked; if (!matchId) { displayError("Match ID is required"); return; } await fetchData("/api/matchInfo", { matchId, ssoToken, platform, game, sanitize, replaceKeys }); }); // Fetch user info document.getElementById("fetchUserInfo").addEventListener("click", async () => { const username = document.getElementById("userUsername").value.trim(); const ssoToken = document.getElementById("ssoToken").value.trim(); const platform = document.getElementById("userPlatform").value; const userCall = document.getElementById("userCall").value; const sanitize = document.getElementById("sanitizeOption").checked; const replaceKeys = document.getElementById("replaceKeysOption").checked; // For event feed and identities, username is not required if ( !username && userCall !== "eventFeed" && userCall !== "friendFeed" && userCall !== "identities" ) { displayError("Username is required for this API call"); return; } await fetchData("/api/user", { username, ssoToken, platform, userCall, sanitize, replaceKeys }); }); // Fuzzy search document.getElementById("fuzzySearch").addEventListener("click", async () => { const username = document.getElementById("searchUsername").value.trim(); const ssoToken = document.getElementById("ssoToken").value.trim(); const platform = document.getElementById("searchPlatform").value; const sanitize = document.getElementById("sanitizeOption").checked; const replaceKeys = document.getElementById("replaceKeysOption").checked; if (!username) { displayError("Username is required for search"); return; } await fetchData("/api/search", { username, ssoToken, platform, sanitize, replaceKeys }); }); // YAML conversion function function jsonToYAML(json) { const INDENT_SIZE = 2; function formatValue(value, indentLevel = 0) { const indent = ' '.repeat(indentLevel); if (value === null) return 'null'; if (value === undefined) return ''; if (typeof value === 'string') { // Check if string needs quotes (contains special chars) if (/[:{}[\],&*#?|\-<>=!%@`]/.test(value) || value === '' || !isNaN(value)) { return `"${value.replace(/"/g, '\\"')}"`; } return value; } if (typeof value === 'number' || typeof value === 'boolean') { return value.toString(); } if (Array.isArray(value)) { if (value.length === 0) return '[]'; let result = ''; for (const item of value) { result += `\n${indent}- ${formatValue(item, indentLevel + INDENT_SIZE).trimStart()}`; } return result; } if (typeof value === 'object') { if (Object.keys(value).length === 0) return '{}'; let result = ''; for (const [key, val] of Object.entries(value)) { const formattedValue = formatValue(val, indentLevel + INDENT_SIZE); // If the formatted value is a multi-line value (array or object), add a line break if (formattedValue.includes('\n')) { result += `\n${indent}${key}:${formattedValue}`; } else { result += `\n${indent}${key}: ${formattedValue}`; } } return result; } return String(value); } return formatValue(json, 0).substring(1); // Remove first newline } // Common fetch function async function fetchData(endpoint, requestData) { console.log(`[CLIENT] Request to ${endpoint} at ${new Date().toISOString()}`); console.log(`[CLIENT] Request data: ${JSON.stringify({ ...requestData, ssoToken: requestData.ssoToken ? requestData.ssoToken.substring(0, 5) + '...' : 'none' })}`); const errorElement = document.getElementById("error"); const loadingElement = document.getElementById("loading"); const resultsElement = document.getElementById("results"); // Reset display errorElement.textContent = ""; resultsElement.style.display = "none"; loadingElement.style.display = "block"; // Hide tutorial if not already dismissed if (!tutorialDismissed) { tutorialDismissed = true; document.querySelectorAll(".tutorial").forEach(element => { element.style.display = "none"; }); } // Validate request data if (!requestData.ssoToken) { displayError("SSO Token is required"); loadingElement.style.display = "none"; return; } try { // Set up the request with a timeout const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), 30000); // 30 second timeout const response = await fetch(endpoint, { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify(requestData), signal: controller.signal }); clearTimeout(timeoutId); // Handle non-JSON responses const contentType = response.headers.get("content-type"); if (!contentType || !contentType.includes("application/json")) { throw new Error("Server returned non-JSON response"); } const data = await response.json(); console.log(`[CLIENT] Response received at ${new Date().toISOString()}`); console.log(`[CLIENT] Response status: ${response.status}`); console.log(`[CLIENT] Response size: ~${JSON.stringify(data).length / 1024} KB`); if (!response.ok) { throw new Error(data.message || `Error: ${response.status}`); } if (data.error) { displayError(data.error); } else if (data.status === "error") { displayError(data.message || "An error occurred"); } else { currentData = data; displayResults(data); } } catch (error) { if (error.name === 'AbortError') { displayError("Request timed out. Please try again."); } else { displayError( `Error: ${error.message || "An error occurred while fetching data."}` ); console.error("Fetch error:", error); } } finally { loadingElement.style.display = "none"; } } // Function to handle time and duration conversion function displayResults(data) { const resultsElement = document.getElementById("results"); const downloadContainer = document.getElementById("download-container"); // Apply time conversion if enabled const convertTime = document.getElementById('convertTimeOption').checked; const replaceKeys = document.getElementById('replaceKeysOption').checked; let displayData = data; if (convertTime || replaceKeys) { const timezone = document.getElementById('timezoneSelect').value; displayData = processTimestamps(structuredClone(data), timezone); // Use structured clone API instead of JSON.parse/stringify // displayData = processTimestamps(JSON.parse(JSON.stringify(data)), timezone); } // Format the data let formattedData = ''; if (outputFormat === 'yaml') { formattedData = jsonToYAML(displayData); document.getElementById("downloadJson").textContent = "Download YAML Data"; } else { formattedData = JSON.stringify(displayData, null, 2); document.getElementById("downloadJson").textContent = "Download JSON Data"; } resultsElement.textContent = formattedData; resultsElement.style.display = "block"; downloadContainer.style.display = "block"; } // Helper function to display errors function displayError(message) { const errorElement = document.getElementById("error"); const loadingElement = document.getElementById("loading"); const resultsElement = document.getElementById("results"); errorElement.textContent = message; loadingElement.style.display = "none"; // Clear previous results to ensure they can be redrawn resultsElement.style.display = "none"; resultsElement.textContent = ""; // Keep tutorial hidden if previously dismissed if (tutorialDismissed) { document.querySelectorAll(".tutorial").forEach(element => { element.style.display = "none"; }); } } function addEnterKeyListeners() { // Use event delegation for handling Enter key press document.addEventListener("keypress", function(event) { if (event.key === "Enter") { // Get the active element const activeElement = document.activeElement; if (!activeElement || !activeElement.id) return; // Mapping of input fields to their submit buttons const inputToButtonMapping = { "ssoToken": null, // Will trigger active tab button "username": null, // Will trigger active tab button "matchUsername": "fetchMatches", "matchId": "fetchMatchInfo", "userUsername": "fetchUserInfo", "searchUsername": "fuzzySearch" }; if (activeElement.id in inputToButtonMapping) { if (inputToButtonMapping[activeElement.id]) { // Click the specific button document.getElementById(inputToButtonMapping[activeElement.id]).click(); } else { // Trigger the active tab button triggerActiveTabButton(); } } } }); } function triggerActiveTabButton() { const activeTab = document.querySelector(".tab.active").getAttribute("data-tab"); switch (activeTab) { case "stats": document.getElementById("fetchStats").click(); break; case "matches": document.getElementById("fetchMatches").click(); break; case "user": document.getElementById("fetchUserInfo").click(); break; case "other": document.getElementById("fuzzySearch").click(); break; } } // Function to convert seconds to human readable duration function formatDuration(seconds) { if (!seconds || isNaN(seconds)) return seconds; // Convert to number in case it's a string const totalSeconds = parseFloat(seconds); // Calculate days, hours, minutes, seconds const days = Math.floor(totalSeconds / 86400); const hours = Math.floor((totalSeconds % 86400) / 3600); const minutes = Math.floor((totalSeconds % 3600) / 60); const remainingSeconds = Math.floor(totalSeconds % 60); return `${days} Days ${hours} Hours ${minutes} Minutes ${remainingSeconds} Seconds`; } // Function to convert epoch time to human-readable format function formatEpochTime(epoch, timezone) { if (!epoch) return epoch; // Check if epoch is in milliseconds (13 digits) or seconds (10 digits) const epochNumber = parseInt(epoch); if (isNaN(epochNumber)) return epoch; // Convert to milliseconds if needed const epochMs = epochNumber.toString().length <= 10 ? epochNumber * 1000 : epochNumber; // Parse the timezone offset let offset = 0; if (timezone !== 'UTC') { const match = timezone.match(/GMT([+-])(\d+)(?::(\d+))?/); if (match) { const sign = match[1] === '+' ? 1 : -1; const hours = parseInt(match[2]); const minutes = match[3] ? parseInt(match[3]) : 0; offset = sign * (hours * 60 + minutes) * 60 * 1000; } } // Create a date object and adjust for timezone const date = new Date(epochMs + offset); // Format the date return date.toUTCString().replace('GMT', timezone); } // Function to recursively process timestamps and durations in the data function processTimestamps(data, timezone, keysToConvert = ['date', 'dateAdded', 'utcStartSeconds', 'utcEndSeconds', 'timestamp', 'startTime', 'endTime'], durationKeys = ['time', 'timePlayedTotal', 'timePlayed', 'avgLifeTime', 'duration', 'objTime']) { if (!data || typeof data !== 'object') return data; if (Array.isArray(data)) { return data.map(item => processTimestamps(item, timezone, keysToConvert, durationKeys)); } const result = {}; for (const [key, value] of Object.entries(data)) { if (keysToConvert.includes(key) && typeof value === 'number') { result[key] = formatEpochTime(value, timezone); } else if (durationKeys.includes(key) && typeof value === 'number' && document.getElementById("replaceKeysOption").checked) { result[key] = formatDuration(value); } else if (typeof value === 'object' && value !== null) { result[key] = processTimestamps(value, timezone, keysToConvert, durationKeys); } else { result[key] = value; } } return result; } // Time options function setupTimeOptions() { const convertTimeCheckbox = document.getElementById('convertTimeOption'); const timezoneSelect = document.getElementById('timezoneSelect'); convertTimeCheckbox.addEventListener('change', function() { timezoneSelect.disabled = !this.checked; if (currentData) { displayResults(currentData); // Refresh the display } }); timezoneSelect.addEventListener('change', function() { if (currentData) { displayResults(currentData); // Refresh the display } }); } // Download Button function setupDownloadButton() { const downloadBtn = document.getElementById("downloadJson"); if (!downloadBtn) return; downloadBtn.addEventListener("click", function() { const resultsElement = document.getElementById("results"); const jsonData = resultsElement.textContent; if (!jsonData) { alert("No data to download"); return; } // Create a Blob with the data const contentType = outputFormat === 'yaml' ? 'text/yaml' : 'application/json'; const blob = new Blob([jsonData], { type: contentType }); // Create a temporary link element const a = document.createElement("a"); a.href = URL.createObjectURL(blob); // Generate a filename with timestamp const date = new Date(); const timestamp = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}_${String(date.getHours()).padStart(2, '0')}-${String(date.getMinutes()).padStart(2, '0')}`; const extension = outputFormat === 'yaml' ? 'yaml' : 'json'; a.download = `cod_stats_${timestamp}.${extension}`; // Trigger download document.body.appendChild(a); a.click(); // Clean up document.body.removeChild(a); }); } // Function to synchronize username across tabs function syncUsernames() { const mainUsername = document.getElementById("username").value.trim(); // Only sync if there's a value if (mainUsername) { document.getElementById("matchUsername").value = mainUsername; document.getElementById("userUsername").value = mainUsername; document.getElementById("searchUsername").value = mainUsername; } // Also sync platform across tabs when it changes const mainPlatform = document.getElementById("platform").value; document.getElementById("matchPlatform").value = mainPlatform; document.getElementById("userPlatform").value = mainPlatform; document.getElementById("searchPlatform").value = mainPlatform; } // Sync listeners for persistent usernames function addSyncListeners() { // Add change listeners for username sync document.getElementById("username").addEventListener("change", syncUsernames); document.getElementById("matchUsername").addEventListener("change", function() { document.getElementById("username").value = this.value; syncUsernames(); }); document.getElementById("userUsername").addEventListener("change", function() { document.getElementById("username").value = this.value; syncUsernames(); }); document.getElementById("searchUsername").addEventListener("change", function() { document.getElementById("username").value = this.value; syncUsernames(); }); // Add change listeners for platform sync document.getElementById("platform").addEventListener("change", syncUsernames); } // 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 }); }); });