838 lines
29 KiB
JavaScript
838 lines
29 KiB
JavaScript
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, '{')
|
||
.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 = `<p>Error rendering graph: ${errorMessage}</p>
|
||
<p>Please try a different URL or report this issue.</p>`;
|
||
|
||
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 = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
||
<path d="M4 1.5H3a2 2 0 0 0-2 2V14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V3.5a2 2 0 0 0-2-2h-1v1h1a1 1 0 0 1 1 1V14a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V3.5a1 1 0 0 1 1-1h1v-1z"/>
|
||
<path d="M9.5 1h-3a.5.5 0 0 0-.5.5v1a.5.5 0 0 0 .5.5h3a.5.5 0 0 0 .5-.5v-1a.5.5 0 0 0-.5-.5zm-3-1A1.5 1.5 0 0 0 5 1.5v1A1.5 1.5 0 0 0 6.5 4h3A1.5 1.5 0 0 0 11 2.5v-1A1.5 1.5 0 0 0 9.5 0h-3z"/>
|
||
</svg>`;
|
||
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 = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
||
<path d="M13.854 3.646a.5.5 0 0 1 0 .708l-7 7a.5.5 0 0 1-.708 0l-3.5-3.5a.5.5 0 1 1 .708-.708L6.5 10.293l6.646-6.647a.5.5 0 0 1 .708 0z"/>
|
||
</svg>`;
|
||
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 = `<div class="error-message">Graph error: ${sanitizeMermaidText(message)}</div>`;
|
||
}
|
||
}
|
||
}); |