document.addEventListener('DOMContentLoaded', () => { const urlForm = document.getElementById('urlForm'); const urlInput = document.getElementById('urlInput'); const httpMethodSelect = document.getElementById('httpMethod'); const userAgentSelect = document.getElementById('userAgent'); const loading = document.getElementById('loading'); const results = document.getElementById('results'); const redirectList = document.getElementById('redirectList'); const errorDiv = document.getElementById('error'); const errorMessage = document.querySelector('.error-message'); const warningsDiv = document.getElementById('warnings'); const warningContainer = document.querySelector('.warning-container'); const summaryContainer = document.querySelector('.summary-container'); const toggleModeBtn = document.getElementById('toggleMode'); const printResultsBtn = document.getElementById('printResults'); const mermaidGraph = document.getElementById('mermaid-graph'); // Check if all required elements exist if (!urlForm || !urlInput || !redirectList || !errorMessage || !warningContainer || !summaryContainer) { console.error('One or more required HTML elements not found. Check your HTML structure.'); return; } // Hide the graph view tab const graphTabButton = document.querySelector('.tab-main[data-tab="graph-view"]'); if (graphTabButton) { graphTabButton.style.display = 'none'; } // Set the list view as the only visible tab const listViewContent = document.getElementById('list-view'); if (listViewContent) { listViewContent.classList.remove('hidden'); } // User agent options const userAgents = { default: '', googlebot: 'Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)', bingbot: 'Mozilla/5.0 (compatible; bingbot/2.0; +http://www.bing.com/bingbot.htm)', chrome: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113 Safari/537.36)', iphone: 'Mozilla/5.0 (iPhone; CPU iPhone OS 15_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.4 Mobile/15E148 Safari/604.1' }; // Set up tab switching const tabButtons = document.querySelectorAll('.tab-main'); tabButtons.forEach(button => { button.addEventListener('click', () => { const tabId = button.getAttribute('data-tab'); // Skip processing if it's the graph-view tab if (tabId === 'graph-view') { return; } // Remove active class from all buttons and content tabButtons.forEach(btn => btn.classList.remove('active')); document.querySelectorAll('.tab-main-content').forEach(content => { content.classList.add('hidden'); }); // Add active class to clicked button and show content button.classList.add('active'); document.getElementById(tabId).classList.remove('hidden'); // Re-render mermaid graph if graph view is selected if (tabId === 'graph-view') { console.log("Switched to graph view tab"); renderMermaidGraph(); } }); }); // Toggle dark mode if (toggleModeBtn) { // Function to toggle dark mode const toggleDarkMode = (isDark) => { if (isDark) { document.body.classList.add('dark-mode'); toggleModeBtn.setAttribute('aria-pressed', 'true'); localStorage.setItem('darkMode', 'enabled'); } else { document.body.classList.remove('dark-mode'); toggleModeBtn.setAttribute('aria-pressed', 'false'); localStorage.setItem('darkMode', 'disabled'); } }; // Check for system preference const prefersDarkScheme = window.matchMedia('(prefers-color-scheme: dark)'); // Check if dark mode was previously set const storedTheme = localStorage.getItem('darkMode'); if (storedTheme === 'disabled') { // User explicitly disabled dark mode toggleDarkMode(false); } else { // Default to dark mode for all other cases: // - When no stored preference // - When stored preference is "enabled" toggleDarkMode(true); } // Initialize ARIA attributes toggleModeBtn.setAttribute('aria-pressed', document.body.classList.contains('dark-mode').toString()); // Add event listener for toggle button toggleModeBtn.addEventListener('click', () => { const isDarkMode = document.body.classList.contains('dark-mode'); toggleDarkMode(!isDarkMode); }); // Also listen for system changes prefersDarkScheme.addEventListener('change', (e) => { if (localStorage.getItem('darkMode') === null) { // Only auto-switch if user hasn't set a preference toggleDarkMode(e.matches); } }); } // Print results printResultsBtn.addEventListener('click', () => { window.print(); }); urlForm.addEventListener('submit', async (e) => { e.preventDefault(); const url = urlInput.value.trim(); const method = httpMethodSelect.value; const userAgent = userAgents[userAgentSelect.value] || ''; if (!url) { showError('Please enter a valid URL'); return; } // Clear previous results redirectList.innerHTML = ''; hideError(); hideWarnings(); showLoading(); hideResults(); try { const response = await fetch('/api/track', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ url, method, userAgent }) }); const data = await response.json(); if (!response.ok) { throw new Error(data.error || 'Failed to track redirects'); } // Check for potential issues with the redirect chain checkRedirectIssues(data.redirects); // Generate and display redirect summary generateRedirectSummary(data.redirects); // Generate Mermaid graph generateMermaidGraph(data.redirects); // Display the redirects in list format displayRedirects(data.redirects); } catch (error) { showError(error.message); } finally { hideLoading(); } }); function checkRedirectIssues(redirects) { // Clear previous warnings warningContainer.innerHTML = ''; let warningCount = 0; // Loop detection const urlSet = new Set(); const loopUrl = redirects.find(redirect => { if (urlSet.has(redirect.url)) { return true; } urlSet.add(redirect.url); return false; }); if (loopUrl) { addWarning(`⚠️ Redirect loop detected! URL "${loopUrl.url}" appears multiple times in the chain.`); warningCount++; } // SSL downgrade detection for (let i = 1; i < redirects.length; i++) { const prevUrl = redirects[i-1].url; const currUrl = redirects[i].url; if (prevUrl.toLowerCase().startsWith('https://') && currUrl.toLowerCase().startsWith('http://')) { addWarning(`⚠️ Mixed content warning: SSL downgrade from "${prevUrl}" to "${currUrl}"`); warningCount++; } } // Show warnings if any if (warningCount > 0) { showWarnings(); } } function generateRedirectSummary(redirects) { // Create a summary of status codes const statusCounts = {}; redirects.forEach(redirect => { if (redirect.statusCode) { const statusKey = `${redirect.statusCode} ${redirect.statusText || ''}`; statusCounts[statusKey] = (statusCounts[statusKey] || 0) + 1; } }); // Create summary text let summaryText = `${redirects.length} step${redirects.length === 1 ? '' : 's'} in chain: `; const statusSummary = Object.entries(statusCounts) .map(([status, count]) => `${count}× ${status.trim()}`) .join(', '); if (statusSummary) { summaryText += statusSummary; } else { summaryText += 'No redirects'; } // Add SSL info const sslCount = redirects.filter(r => r.isSSL).length; const nonSslCount = redirects.filter(r => !r.isSSL).length; if (sslCount > 0 || nonSslCount > 0) { summaryText += ` (${sslCount} SSL, ${nonSslCount} non-SSL)`; } summaryContainer.textContent = summaryText; } function generateMermaidGraph(redirects) { console.log('Generating Mermaid graph from redirects:', redirects); if (!redirects || redirects.length === 0) { console.log('No redirects to display in graph'); return 'graph TD\n emptyNode["No redirects to display"]\n style emptyNode fill:#f9f9f9,stroke:#ccc,stroke-width:1px'; } let definition = 'graph TD\n'; // Track visited URLs to avoid duplicates const visitedUrls = new Set(); // First pass - add all nodes redirects.forEach((redirect, index) => { if (!redirect.url || !redirect.redirectUrl) return; const fromUrl = sanitizeMermaidText(redirect.url); const toUrl = sanitizeMermaidText(redirect.redirectUrl); // Skip if we've already processed this exact redirect const redirectKey = `${fromUrl}|${toUrl}`; if (visitedUrls.has(redirectKey)) return; visitedUrls.add(redirectKey); // Create node labels with truncated URLs const fromLabel = truncateUrl(redirect.url, 30); const toLabel = truncateUrl(redirect.redirectUrl, 30); // Add nodes with ID based on index to avoid duplicate IDs and label issues definition += ` node${index}["${sanitizeMermaidText(fromLabel)}"]\n`; definition += ` node${index}to["${sanitizeMermaidText(toLabel)}"]\n`; // Add connection with status code if available const statusText = redirect.statusCode ? ` (${redirect.statusCode})` : ''; definition += ` node${index} -->|"${statusText}"| node${index}to\n`; }); console.log('Generated Mermaid definition:', definition); return definition; } // Separate function to handle Mermaid rendering function renderMermaidGraph() { // First make sure Mermaid is properly loaded if (typeof mermaid === 'undefined') { console.error('Mermaid library not loaded'); displayGraphError('Graph rendering library not loaded properly.'); return; } // Reset and reinitialize Mermaid with current theme try { const isDarkMode = document.body.classList.contains('dark-mode'); console.log("Initializing Mermaid with theme:", isDarkMode ? 'dark' : 'default'); mermaid.initialize({ startOnLoad: false, theme: isDarkMode ? 'dark' : 'default', securityLevel: 'loose', logLevel: 'warn', // Change to 'debug' for more detailed logs fontFamily: 'sans-serif', flowchart: { useMaxWidth: true, htmlLabels: true, curve: 'basis' } }); // Wait a bit to ensure DOM is ready setTimeout(() => { try { console.log("Running mermaid.run()"); mermaid.run({ querySelector: '.mermaid' }).then(() => { console.log("Mermaid graph rendered successfully"); }).catch(err => { console.error("Mermaid rendering error:", err); displayGraphError('Failed to render graph: ' + err.message); }); } catch (err) { console.error('Error during mermaid.run():', err); // Fallback approach try { console.log("Falling back to contentLoaded()"); mermaid.contentLoaded(); } catch (innerErr) { console.error('Error during fallback rendering:', innerErr); displayGraphError('Failed to render graph. Technical details: ' + innerErr.message); } } }, 200); } catch (e) { console.error('Error initializing Mermaid:', e); displayGraphError('Failed to initialize graph rendering: ' + e.message); } } // Sanitize text for Mermaid compatibility function sanitizeMermaidText(text) { if (!text) return ''; // Escape characters that can break Mermaid syntax return text .replace(/"/g, "'") // Replace double quotes with single quotes .replace(/&/g, '&') .replace(//g, '>') .replace(/\(/g, '(') .replace(/\)/g, ')') .replace(/\[/g, '[') .replace(/\]/g, ']') .replace(/\{/g, '{') .replace(/\}/g, '}'); } // Helper function to truncate long URLs for display function truncateUrl(url, maxLength) { if (!url) return ''; try { // Try to parse and display in a more readable format const urlObj = new URL(url); const displayUrl = urlObj.hostname + urlObj.pathname; if (displayUrl.length <= maxLength) { return displayUrl; } return displayUrl.substring(0, maxLength - 3) + '...'; } catch (e) { // Fallback if URL parsing fails return url.length > maxLength ? url.substring(0, maxLength - 3) + '...' : url; } } // Display error message in the graph container function displayGraphError(errorMessage) { mermaidGraph.innerHTML = ''; const errorDiv = document.createElement('div'); errorDiv.className = 'mermaid-error'; errorDiv.innerHTML = `
Error rendering graph: ${errorMessage}
Please try a different URL or report this issue.
`; mermaidGraph.appendChild(errorDiv); } function displayRedirects(redirects) { if (!redirects || redirects.length === 0) { showError('No redirect information received'); return; } redirects.forEach((redirect, index) => { const li = document.createElement('li'); li.className = 'redirect-item'; if (!redirect.isSSL) { li.classList.add('non-ssl'); } if (redirect.final) { li.classList.add('final'); } // Create header with step info, status code, and duration const header = document.createElement('div'); header.className = 'redirect-item-header'; // Step and status const stepLabel = document.createElement('span'); const statusCode = redirect.statusCode ? ` (${redirect.statusCode} ${redirect.statusText || ''})` : ''; stepLabel.textContent = `Step ${index}:${statusCode}`; stepLabel.className = 'step-label'; // Duration const duration = document.createElement('span'); duration.className = 'redirect-duration'; const durationValue = redirect.duration !== undefined ? redirect.duration : 0; duration.textContent = `${durationValue} ms`; header.appendChild(stepLabel); header.appendChild(duration); // URL container with copy button const urlContainer = document.createElement('div'); urlContainer.className = 'url-container'; // URL text const urlDiv = document.createElement('div'); urlDiv.className = 'redirect-url'; urlDiv.textContent = redirect.url; // Copy button const copyBtn = document.createElement('button'); copyBtn.className = 'copy-url-btn'; copyBtn.innerHTML = ``; copyBtn.setAttribute('aria-label', 'Copy URL to clipboard'); copyBtn.setAttribute('title', 'Copy URL to clipboard'); // Add copy functionality copyBtn.addEventListener('click', (e) => { e.stopPropagation(); // Prevent toggling details when clicking the copy button // Copy URL to clipboard navigator.clipboard.writeText(redirect.url) .then(() => { // Show success feedback const originalHTML = copyBtn.innerHTML; copyBtn.innerHTML = ``; copyBtn.classList.add('copied'); // Reset button after a delay setTimeout(() => { copyBtn.innerHTML = originalHTML; copyBtn.classList.remove('copied'); }, 2000); }) .catch(err => { console.error('Could not copy URL: ', err); alert('Failed to copy URL. Please try again.'); }); }); urlContainer.appendChild(urlDiv); urlContainer.appendChild(copyBtn); li.appendChild(header); li.appendChild(urlContainer); // Create collapsible section for details const detailsContainer = document.createElement('div'); detailsContainer.className = 'details-container'; // Create toggle button const toggleButton = document.createElement('button'); toggleButton.className = 'toggle-details-btn'; toggleButton.textContent = 'Show Details'; toggleButton.addEventListener('click', () => { const detailsContent = detailsContainer.querySelector('.details-content'); const isHidden = detailsContent.classList.contains('hidden'); if (isHidden) { detailsContent.classList.remove('hidden'); toggleButton.textContent = 'Hide Details'; } else { detailsContent.classList.add('hidden'); toggleButton.textContent = 'Show Details'; } }); // Create details content (hidden by default) const detailsContent = document.createElement('div'); detailsContent.className = 'details-content hidden'; // Add tabs for different types of info const tabsContainer = document.createElement('div'); tabsContainer.className = 'tabs-container'; const tabs = [ { id: 'headers', name: 'Headers' }, { id: 'body', name: 'Response Body' }, { id: 'metadata', name: 'Metadata' }, { id: 'tracking', name: 'Tracking Params' }, { id: 'ssl', name: 'SSL Info' } ]; const tabButtons = document.createElement('div'); tabButtons.className = 'tab-buttons'; const tabContents = document.createElement('div'); tabContents.className = 'tab-contents'; tabs.forEach((tab, tabIndex) => { // Create tab button const tabButton = document.createElement('button'); tabButton.className = 'tab-button'; tabButton.textContent = tab.name; if (tabIndex === 0) tabButton.classList.add('active'); // Create tab content const tabContent = document.createElement('div'); tabContent.className = 'tab-content'; tabContent.id = `${tab.id}-${index}`; if (tabIndex !== 0) tabContent.classList.add('hidden'); // Add content based on tab type switch (tab.id) { case 'headers': if (redirect.metadata && redirect.metadata.headers) { const headersContent = document.createElement('pre'); headersContent.className = 'headers-content'; // Format headers let headersText = ''; const headers = redirect.metadata.headers; for (const key in headers) { headersText += `${key}: ${headers[key]}\n`; } headersContent.textContent = headersText || 'No headers available'; tabContent.appendChild(headersContent); } else { tabContent.textContent = 'No headers available'; } break; case 'body': const bodyContent = document.createElement('pre'); bodyContent.className = 'body-content'; bodyContent.textContent = redirect.responseBody || 'No response body available'; tabContent.appendChild(bodyContent); break; case 'metadata': if (redirect.metadata) { const metadataContent = document.createElement('pre'); metadataContent.className = 'metadata-content'; metadataContent.textContent = JSON.stringify(redirect.metadata, null, 2); tabContent.appendChild(metadataContent); } else { tabContent.textContent = 'No metadata available'; } break; case 'tracking': // Extract tracking parameters from the URL const trackingParams = extractTrackingParams(redirect.url); if (Object.keys(trackingParams).length > 0) { // Create a table for better presentation const trackingTable = document.createElement('table'); trackingTable.className = 'tracking-table'; // Add table header const tableHeader = document.createElement('thead'); const headerRow = document.createElement('tr'); const headerParam = document.createElement('th'); headerParam.textContent = 'Parameter'; headerRow.appendChild(headerParam); const headerValue = document.createElement('th'); headerValue.textContent = 'Value'; headerRow.appendChild(headerValue); tableHeader.appendChild(headerRow); trackingTable.appendChild(tableHeader); // Add table body const tableBody = document.createElement('tbody'); Object.entries(trackingParams).forEach(([param, value]) => { const row = document.createElement('tr'); const paramCell = document.createElement('td'); paramCell.className = 'param-name'; paramCell.textContent = param; row.appendChild(paramCell); const valueCell = document.createElement('td'); valueCell.className = 'param-value'; valueCell.textContent = value; row.appendChild(valueCell); tableBody.appendChild(row); }); trackingTable.appendChild(tableBody); tabContent.appendChild(trackingTable); } else { tabContent.textContent = 'No tracking parameters detected in this URL'; } break; case 'ssl': if (redirect.sslInfo) { const sslContent = document.createElement('pre'); sslContent.className = 'ssl-content'; sslContent.textContent = JSON.stringify(redirect.sslInfo, null, 2); tabContent.appendChild(sslContent); } else { tabContent.textContent = redirect.isSSL ? 'SSL certificate information not available' : 'Not an SSL connection'; } break; } // Add click handler for tab button tabButton.addEventListener('click', () => { // Deactivate all buttons and hide all contents tabButtons.querySelectorAll('.tab-button').forEach(btn => btn.classList.remove('active')); tabContents.querySelectorAll('.tab-content').forEach(content => content.classList.add('hidden')); // Activate this button and show its content tabButton.classList.add('active'); tabContent.classList.remove('hidden'); }); tabButtons.appendChild(tabButton); tabContents.appendChild(tabContent); }); tabsContainer.appendChild(tabButtons); tabsContainer.appendChild(tabContents); // Add error message if present if (redirect.error) { const errorInfo = document.createElement('div'); errorInfo.className = 'redirect-error'; errorInfo.textContent = `Error: ${redirect.error}`; detailsContent.appendChild(errorInfo); } detailsContent.appendChild(tabsContainer); detailsContainer.appendChild(toggleButton); detailsContainer.appendChild(detailsContent); li.appendChild(detailsContainer); redirectList.appendChild(li); }); showResults(); } function addWarning(message) { const warningDiv = document.createElement('div'); warningDiv.textContent = message; warningContainer.appendChild(warningDiv); } function showWarnings() { warningsDiv.classList.remove('hidden'); } function hideWarnings() { warningsDiv.classList.add('hidden'); warningContainer.innerHTML = ''; } function showLoading() { loading.classList.remove('hidden'); } function hideLoading() { loading.classList.add('hidden'); } function showResults() { results.classList.remove('hidden'); } function hideResults() { results.classList.add('hidden'); } function showError(message) { errorMessage.textContent = message; errorDiv.classList.remove('hidden'); } function hideError() { errorDiv.classList.add('hidden'); } // Add a helper function to extract tracking parameters from URL function extractTrackingParams(url) { try { const parsedUrl = new URL(url); const searchParams = parsedUrl.searchParams; const trackingParams = {}; // Common tracking parameters to look for const knownParams = [ // UTM parameters { key: 'utm_source', label: 'Source' }, { key: 'utm_medium', label: 'Medium' }, { key: 'utm_campaign', label: 'Campaign' }, { key: 'utm_term', label: 'Term' }, { key: 'utm_content', label: 'Content' }, // Facebook parameters { key: 'fbclid', label: 'Facebook Click ID' }, // Google parameters { key: 'gclid', label: 'Google Click ID' }, { key: 'gclsrc', label: 'Google Click Source' }, // Other common parameters { key: 'source', label: 'Source' }, { key: 'medium', label: 'Medium' }, { key: 'campaign', label: 'Campaign' }, { key: 'term', label: 'Term' }, { key: 'content', label: 'Content' }, { key: 'referrer', label: 'Referrer' }, { key: 'ref', label: 'Referral' }, { key: 'affiliate', label: 'Affiliate' }, { key: 'channel', label: 'Channel' }, // Microsoft Advertising { key: 'msclkid', label: 'Microsoft Click ID' }, // TikTok { key: 'ttclid', label: 'TikTok Click ID' } ]; // Check for known parameters knownParams.forEach(param => { if (searchParams.has(param.key)) { trackingParams[param.label] = searchParams.get(param.key); } }); // Also collect any other parameters that look like tracking params for (const [key, value] of searchParams.entries()) { if (!Object.values(trackingParams).includes(value)) { if ( key.includes('utm_') || key.includes('ref') || key.includes('source') || key.includes('medium') || key.includes('campaign') || key.includes('clid') || key.includes('tracking') || key.includes('ad') ) { trackingParams[key] = value; } } } return trackingParams; } catch (e) { console.error('Error parsing URL tracking params:', e); return {}; } } function updateMermaidGraph(redirects) { const mermaidGraph = document.getElementById('mermaidGraph'); if (!mermaidGraph) { console.error('Mermaid graph container not found'); return; } try { // Clear existing graph mermaidGraph.innerHTML = ''; // Get the graph definition const graphDefinition = generateMermaidGraph(redirects); // Create a new div element for Mermaid const newDiv = document.createElement('div'); newDiv.className = 'mermaid'; newDiv.textContent = graphDefinition; // Add the div to the container mermaidGraph.appendChild(newDiv); // Render the graph renderMermaidGraph(); } catch (e) { console.error('Error updating Mermaid graph:', e); displayGraphError(e.message); } } function displayGraphError(message) { const mermaidGraph = document.getElementById('mermaidGraph'); if (mermaidGraph) { mermaidGraph.innerHTML = ``; } } });