Initial commit: URL Redirect Tracker application with comprehensive documentation
This commit is contained in:
838
public/script.js
Normal file
838
public/script.js
Normal file
@@ -0,0 +1,838 @@
|
||||
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>`;
|
||||
}
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user