diff --git a/.gitignore b/.gitignore
index 06a75af4b65b66c59b6f5ae7c2f33f8f22e24cc7..3e973e62289455910dd4a938d062aa0f339176bf 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1,2 @@
-postgres/
\ No newline at end of file
+postgres/
+*.csv
diff --git a/visualize.html b/visualize.html
new file mode 100644
index 0000000000000000000000000000000000000000..6febae2549d22b36c3cfc3b4f8f92761a829a415
--- /dev/null
+++ b/visualize.html
@@ -0,0 +1,285 @@
+<!DOCTYPE html>
+<html lang="en">
+
+<head>
+    <meta charset="UTF-8">
+    <title>Performance Visualization</title>
+    <script src="https://cdnjs.cloudflare.com/ajax/libs/PapaParse/5.4.1/papaparse.js"></script>
+    <script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/3.7.1/chart.js"></script>
+    <script src="https://cdn.jsdelivr.net/npm/chartjs-adapter-date-fns"></script>
+
+
+    <style>
+        body {
+            font-family: Arial, sans-serif;
+            margin: 0 auto;
+            padding: 20px;
+        }
+
+        #charts-container {
+            display: flex;
+            width: 100%;
+            flex-direction: column;
+            gap: 50px;
+        }
+
+        #chart-container,
+        #error-chart-container {
+            width: 100%;
+            height: 600px;
+        }
+
+        canvas {
+            display: block;
+        }
+    </style>
+</head>
+
+<body>
+    <div id="charts-container">
+        <div>
+            <h2>Latency `requests.get(url, timeout=20)`</h2>
+            <button id="filter60">Filter sub-60s response time</button>
+            <button id="filter10">Filter sub-10s response time</button>
+            <button id="filter1">Filter sub-1s response time</button>
+            <button id="show100">Show 100%</button>
+            <button id="show10">Show 10%</button>
+            <button id="show5">Show 5%</button>
+            <div id="chart-container">
+                <canvas id="performanceChart"></canvas>
+            </div>
+        </div>
+
+
+        <h2>Error Rate Over Time (HTTP status != 200)</h2>
+        <div id="error-chart-container">
+            <canvas id="errorRateChart"></canvas>
+        </div>
+    </div>
+
+    <script>
+        // Ensure Papa is loaded before running the script
+        function initVisualization() {
+            let outlierFilter = 10; // Initial state for the 10s limit
+            let showMod = 10;
+            let chart; // Reference to the chart instance
+
+            fetch('exported.csv')
+                .then(response => response.text())
+                .then(csvText => {
+                    const parsed = Papa.parse(csvText, { header: true, dynamicTyping: true });
+
+                    document.getElementById('filter60').addEventListener('click', () => {
+                        outlierFilter = 60;
+                        updateChart();
+                    });
+                    document.getElementById('filter10').addEventListener('click', () => {
+                        outlierFilter = 10;
+                        updateChart();
+                    });
+                    document.getElementById('filter1').addEventListener('click', () => {
+                        outlierFilter = 1;
+                        updateChart();
+                    });
+
+                    document.getElementById('show100').addEventListener('click', () => {
+                        showMod = 1;
+                        updateChart();
+                    });
+                    document.getElementById('show10').addEventListener('click', () => {
+                        showMod = 10;
+                        updateChart();
+                    });
+                    document.getElementById('show5').addEventListener('click', () => {
+                        showMod = 20;
+                        updateChart();
+                    });
+
+                    function updateChart() {
+                        const filteredData = parsed.data.filter(row => row.duration <= outlierFilter);
+
+                        const uniqueUrls = [...new Set(filteredData.map(row => row.url))];
+
+                        const datasets = uniqueUrls.map((url, index) => {
+                            const urlData = filteredData.filter(row => row.url === url);
+                            const sampledData = urlData.filter((_, idx) => idx % showMod === 0);
+                            return {
+                                label: url,
+                                data: sampledData.map(row => ({
+                                    x: new Date(row.timestamp),
+                                    y: row.duration
+                                })),
+                                borderColor: `hsl(${index * 360 / uniqueUrls.length}, 70%, 50%)`,
+                                backgroundColor: `hsla(${index * 360 / uniqueUrls.length}, 70%, 50%, 0.1)`,
+                                borderWidth: 1,
+                                pointRadius: 1,
+                                pointHoverRadius: 5
+                            };
+                        });
+
+                        // Update the chart data
+                        chart.data.datasets = datasets;
+                        chart.options.plugins.tooltip = showMod > 10;
+                        chart.update();
+                    }
+
+                    const ctx = document.getElementById('performanceChart').getContext('2d');
+                    chart = new Chart(ctx, {
+                        type: 'scatter',
+                        data: {
+                            datasets: []
+                        },
+                        options: {
+                            responsive: true,
+                            plugins: {
+                                title: {
+                                    display: true,
+                                    text: 'Response Time by URL Over Time'
+                                },
+                                tooltip: {
+                                    enabled: showMod > 10
+                                }
+                            },
+                            scales: {
+                                x: {
+                                    type: 'time',
+                                    time: {
+                                        unit: 'day'
+                                    },
+                                    title: {
+                                        display: true,
+                                        text: 'Timestamp'
+                                    }
+                                },
+                                y: {
+                                    title: {
+                                        display: true,
+                                        text: 'Response Time (seconds)'
+                                    },
+                                    beginAtZero: true
+                                }
+                            },
+                            animation: false
+                        }
+                    });
+
+                    // Initial chart update
+                    updateChart();
+
+                    initErrorRateChart(parsed);
+                })
+                .catch(error => console.error('Error loading CSV:', error));
+
+        }
+
+        function initErrorRateChart(parsed) {
+            // Aggregate error rate per hour
+            const aggregateErrorRate = (data) => {
+                const aggregatedData = {};
+                data.forEach(row => {
+                    if (row.timestamp == null) {
+                        return;
+                    }
+                    const hour = new Date(row.timestamp).toISOString().slice(0, 13); // Format: YYYY-MM-DDTHH
+                    if (!aggregatedData[hour]) {
+                        aggregatedData[hour] = {};
+                    }
+                    if (!aggregatedData[hour][row.url]) {
+                        aggregatedData[hour][row.url] = { total: 0, errors: 0 };
+                    }
+                    aggregatedData[hour][row.url].total += 1;
+                    if (row.status_code !== 200) {
+                        aggregatedData[hour][row.url].errors += 1;
+                    }
+                });
+
+                // Convert aggregated data into a chart-friendly format
+                const chartData = {};
+                //console.log("aggregatedData size", Object.keys(aggregatedData).length);
+                for (const hour in aggregatedData) {
+                    for (const url in aggregatedData[hour]) {
+                        if (!chartData[url]) {
+                            chartData[url] = [];
+                        }
+                        const total = aggregatedData[hour][url].total;
+                        const errors = aggregatedData[hour][url].errors;
+                        // YYYY-MM-DDTHH
+                        const tt = new Date(hour + ":00:00Z");
+                        chartData[url].push({
+                            x: tt,
+                            y: (errors / total) * 100 // Error rate percentage
+                        });
+                    }
+                }
+                // sort by time
+                for (const url in chartData) {
+                    chartData[url].sort((a, b) => a.x - b.x);
+                }
+                return chartData;
+            };
+
+            const errorRateData = aggregateErrorRate(parsed.data);
+
+
+            // Create datasets for the error rate chart
+            const errorRateDatasets = Object.keys(errorRateData).map((url, index) => ({
+                label: url,
+                data: errorRateData[url],
+                borderColor: `hsl(${index * 360 / Object.keys(errorRateData).length}, 70%, 50%)`,
+                backgroundColor: `hsla(${index * 360 / Object.keys(errorRateData).length}, 70%, 50%, 0.1)`,
+                borderWidth: 1,
+                pointRadius: 1,
+                pointHoverRadius: 5
+            }));
+            console.log(errorRateDatasets);
+
+            // Create the error rate chart
+            const errorCtx = document.getElementById('errorRateChart').getContext('2d');
+            new Chart(errorCtx, {
+                type: 'line',
+                data: {
+                    datasets: errorRateDatasets
+                },
+                options: {
+                    responsive: true,
+                    plugins: {
+                        title: {
+                            display: true,
+                            text: 'Error Rate by URL Over Time (per hour)'
+                        }
+                    },
+                    scales: {
+                        x: {
+                            type: 'time',
+                            time: {
+                                unit: 'day'
+                            },
+                            title: {
+                                display: true,
+                                text: 'Timestamp'
+                            }
+                        },
+                        y: {
+                            title: {
+                                display: true,
+                                text: 'Error Rate (%)'
+                            },
+                            beginAtZero: true,
+                            //max: 100
+                        }
+                    },
+                    animation: false
+                }
+            });
+        }
+
+        // Wait for dependencies to load
+        if (window.Papa && window.Chart) {
+            initVisualization();
+        } else {
+            window.addEventListener('load', initVisualization);
+        }
+    </script>
+</body>
+
+</html>
\ No newline at end of file