999 lines
37 KiB
HTML
999 lines
37 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>Web Worker Event Processor - Claude Code DevTools</title>
|
||
<style>
|
||
:root {
|
||
--bg-primary: #0d1117;
|
||
--bg-secondary: #161b22;
|
||
--bg-tertiary: #1c2128;
|
||
--bg-quaternary: #21262d;
|
||
--text-primary: #c9d1d9;
|
||
--text-secondary: #8b949e;
|
||
--text-tertiary: #6e7681;
|
||
--border-primary: #30363d;
|
||
--border-secondary: #21262d;
|
||
--accent-blue: #58a6ff;
|
||
--accent-green: #3fb950;
|
||
--accent-yellow: #d29922;
|
||
--accent-red: #f85149;
|
||
--accent-purple: #bc8cff;
|
||
--shadow: rgba(0, 0, 0, 0.3);
|
||
--font-mono: 'Fira Code', 'Cascadia Code', 'SF Mono', Consolas, monospace;
|
||
}
|
||
|
||
* {
|
||
margin: 0;
|
||
padding: 0;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
body {
|
||
font-family: var(--font-mono);
|
||
background: var(--bg-primary);
|
||
color: var(--text-primary);
|
||
line-height: 1.6;
|
||
min-height: 100vh;
|
||
}
|
||
|
||
header {
|
||
background: var(--bg-secondary);
|
||
border-bottom: 1px solid var(--border-primary);
|
||
padding: 1.5rem 2rem;
|
||
}
|
||
|
||
h1 {
|
||
font-size: 1.75rem;
|
||
color: var(--accent-blue);
|
||
margin-bottom: 0.5rem;
|
||
}
|
||
|
||
.tagline {
|
||
color: var(--text-secondary);
|
||
font-size: 0.95rem;
|
||
}
|
||
|
||
main {
|
||
max-width: 1400px;
|
||
margin: 0 auto;
|
||
padding: 2rem;
|
||
}
|
||
|
||
.section {
|
||
background: var(--bg-secondary);
|
||
border: 1px solid var(--border-primary);
|
||
border-radius: 8px;
|
||
padding: 1.5rem;
|
||
margin-bottom: 1.5rem;
|
||
}
|
||
|
||
.section h2 {
|
||
color: var(--accent-green);
|
||
font-size: 1.25rem;
|
||
margin-bottom: 1rem;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.5rem;
|
||
}
|
||
|
||
.controls {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||
gap: 1rem;
|
||
margin-bottom: 1.5rem;
|
||
}
|
||
|
||
.control-group {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 0.5rem;
|
||
}
|
||
|
||
label {
|
||
color: var(--text-secondary);
|
||
font-size: 0.9rem;
|
||
font-weight: 500;
|
||
}
|
||
|
||
input[type="file"], select, button {
|
||
background: var(--bg-tertiary);
|
||
border: 1px solid var(--border-primary);
|
||
color: var(--text-primary);
|
||
padding: 0.6rem 0.8rem;
|
||
border-radius: 6px;
|
||
font-family: var(--font-mono);
|
||
font-size: 0.9rem;
|
||
transition: all 0.2s;
|
||
}
|
||
|
||
button {
|
||
cursor: pointer;
|
||
background: var(--accent-blue);
|
||
color: #000;
|
||
font-weight: 600;
|
||
border: none;
|
||
}
|
||
|
||
button:hover:not(:disabled) {
|
||
background: #79c0ff;
|
||
transform: translateY(-1px);
|
||
}
|
||
|
||
button:disabled {
|
||
opacity: 0.5;
|
||
cursor: not-allowed;
|
||
}
|
||
|
||
button.secondary {
|
||
background: var(--bg-quaternary);
|
||
color: var(--text-primary);
|
||
border: 1px solid var(--border-primary);
|
||
}
|
||
|
||
button.secondary:hover:not(:disabled) {
|
||
background: var(--bg-tertiary);
|
||
}
|
||
|
||
.worker-status {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.5rem;
|
||
padding: 0.75rem 1rem;
|
||
background: var(--bg-tertiary);
|
||
border-radius: 6px;
|
||
margin-bottom: 1rem;
|
||
}
|
||
|
||
.status-indicator {
|
||
width: 12px;
|
||
height: 12px;
|
||
border-radius: 50%;
|
||
background: var(--text-tertiary);
|
||
}
|
||
|
||
.status-indicator.idle {
|
||
background: var(--accent-blue);
|
||
}
|
||
|
||
.status-indicator.working {
|
||
background: var(--accent-yellow);
|
||
animation: pulse 1s infinite;
|
||
}
|
||
|
||
.status-indicator.complete {
|
||
background: var(--accent-green);
|
||
}
|
||
|
||
.status-indicator.error {
|
||
background: var(--accent-red);
|
||
}
|
||
|
||
@keyframes pulse {
|
||
0%, 100% { opacity: 1; }
|
||
50% { opacity: 0.5; }
|
||
}
|
||
|
||
.progress-container {
|
||
background: var(--bg-tertiary);
|
||
border-radius: 6px;
|
||
padding: 1rem;
|
||
margin-bottom: 1rem;
|
||
}
|
||
|
||
.progress-bar-wrapper {
|
||
background: var(--bg-quaternary);
|
||
border-radius: 4px;
|
||
height: 24px;
|
||
overflow: hidden;
|
||
margin-bottom: 0.5rem;
|
||
}
|
||
|
||
.progress-bar {
|
||
height: 100%;
|
||
background: linear-gradient(90deg, var(--accent-blue), var(--accent-purple));
|
||
transition: width 0.3s ease;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 0.75rem;
|
||
font-weight: 600;
|
||
color: #000;
|
||
}
|
||
|
||
.progress-text {
|
||
font-size: 0.85rem;
|
||
color: var(--text-secondary);
|
||
}
|
||
|
||
.results-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||
gap: 1rem;
|
||
}
|
||
|
||
.result-card {
|
||
background: var(--bg-tertiary);
|
||
border: 1px solid var(--border-primary);
|
||
border-radius: 6px;
|
||
padding: 1rem;
|
||
}
|
||
|
||
.result-card h3 {
|
||
color: var(--accent-purple);
|
||
font-size: 1rem;
|
||
margin-bottom: 0.75rem;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.5rem;
|
||
}
|
||
|
||
.result-item {
|
||
padding: 0.5rem;
|
||
background: var(--bg-quaternary);
|
||
border-radius: 4px;
|
||
margin-bottom: 0.5rem;
|
||
font-size: 0.85rem;
|
||
}
|
||
|
||
.result-item:last-child {
|
||
margin-bottom: 0;
|
||
}
|
||
|
||
.metric {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
}
|
||
|
||
.metric-value {
|
||
color: var(--accent-green);
|
||
font-weight: 600;
|
||
}
|
||
|
||
.pattern-list {
|
||
list-style: none;
|
||
}
|
||
|
||
.pattern-item {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
padding: 0.5rem;
|
||
background: var(--bg-quaternary);
|
||
border-radius: 4px;
|
||
margin-bottom: 0.4rem;
|
||
}
|
||
|
||
.pattern-sequence {
|
||
font-family: var(--font-mono);
|
||
color: var(--accent-blue);
|
||
font-size: 0.8rem;
|
||
}
|
||
|
||
.pattern-count {
|
||
color: var(--accent-yellow);
|
||
font-weight: 600;
|
||
}
|
||
|
||
.docs {
|
||
background: var(--bg-secondary);
|
||
border: 1px solid var(--border-primary);
|
||
border-radius: 8px;
|
||
padding: 1.5rem;
|
||
margin-top: 2rem;
|
||
}
|
||
|
||
.docs h2 {
|
||
color: var(--accent-green);
|
||
margin-bottom: 1rem;
|
||
}
|
||
|
||
.docs h3 {
|
||
color: var(--accent-blue);
|
||
margin-top: 1.5rem;
|
||
margin-bottom: 0.75rem;
|
||
font-size: 1.1rem;
|
||
}
|
||
|
||
.docs ul, .docs ol {
|
||
margin-left: 1.5rem;
|
||
margin-bottom: 1rem;
|
||
}
|
||
|
||
.docs li {
|
||
margin-bottom: 0.5rem;
|
||
color: var(--text-primary);
|
||
}
|
||
|
||
.docs code {
|
||
background: var(--bg-tertiary);
|
||
padding: 0.2rem 0.4rem;
|
||
border-radius: 3px;
|
||
color: var(--accent-purple);
|
||
font-size: 0.9em;
|
||
}
|
||
|
||
.docs a {
|
||
color: var(--accent-blue);
|
||
text-decoration: none;
|
||
}
|
||
|
||
.docs a:hover {
|
||
text-decoration: underline;
|
||
}
|
||
|
||
footer {
|
||
text-align: center;
|
||
padding: 2rem;
|
||
color: var(--text-tertiary);
|
||
font-size: 0.85rem;
|
||
border-top: 1px solid var(--border-primary);
|
||
margin-top: 3rem;
|
||
}
|
||
|
||
footer a {
|
||
color: var(--accent-blue);
|
||
text-decoration: none;
|
||
}
|
||
|
||
.empty-state {
|
||
text-align: center;
|
||
padding: 3rem;
|
||
color: var(--text-tertiary);
|
||
}
|
||
|
||
.sample-data {
|
||
background: var(--bg-tertiary);
|
||
border: 1px solid var(--border-primary);
|
||
border-radius: 6px;
|
||
padding: 1rem;
|
||
margin-top: 1rem;
|
||
}
|
||
|
||
.sample-data pre {
|
||
overflow-x: auto;
|
||
font-size: 0.8rem;
|
||
color: var(--text-secondary);
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<header>
|
||
<h1>⚙️ Web Worker Event Processor</h1>
|
||
<p class="tagline">Background analysis of Claude Code hook events using Web Workers</p>
|
||
</header>
|
||
|
||
<main>
|
||
<section class="section">
|
||
<h2>📥 Load Hook Events</h2>
|
||
<div class="controls">
|
||
<div class="control-group">
|
||
<label for="eventFile">Select Events File (JSON/JSONL)</label>
|
||
<input type="file" id="eventFile" accept=".json,.jsonl">
|
||
</div>
|
||
<div class="control-group">
|
||
<label for="analysisType">Analysis Type</label>
|
||
<select id="analysisType">
|
||
<option value="patterns">Pattern Detection</option>
|
||
<option value="errors">Error Correlation</option>
|
||
<option value="sessions">Session Analysis</option>
|
||
<option value="agents">Agent Comparison</option>
|
||
<option value="anomalies">Anomaly Detection</option>
|
||
<option value="all">Complete Analysis (All)</option>
|
||
</select>
|
||
</div>
|
||
<div class="control-group">
|
||
<label> </label>
|
||
<button id="loadSampleBtn" class="secondary">📊 Load Sample Data</button>
|
||
</div>
|
||
<div class="control-group">
|
||
<label> </label>
|
||
<button id="analyzeBtn" disabled>🚀 Start Analysis</button>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="section">
|
||
<h2>🔄 Worker Status</h2>
|
||
<div class="worker-status">
|
||
<div class="status-indicator" id="workerIndicator"></div>
|
||
<span id="workerStatus">No worker initialized</span>
|
||
</div>
|
||
<div class="progress-container" id="progressContainer" style="display: none;">
|
||
<div class="progress-bar-wrapper">
|
||
<div class="progress-bar" id="progressBar" style="width: 0%">0%</div>
|
||
</div>
|
||
<div class="progress-text" id="progressText">Initializing...</div>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="section">
|
||
<h2>📊 Analysis Results</h2>
|
||
<div id="resultsContainer">
|
||
<div class="empty-state">
|
||
<p>Load event data and run analysis to see results</p>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="docs">
|
||
<h2>About This Tool</h2>
|
||
|
||
<h3>Purpose</h3>
|
||
<p>Process large volumes of Claude Code hook events in the background using Web Workers. Analyze patterns, detect errors, compare agent behavior, and identify anomalies without blocking the UI.</p>
|
||
|
||
<h3>Features</h3>
|
||
<ul>
|
||
<li><strong>Background Processing</strong>: Uses Web Workers to analyze thousands of events without freezing the UI</li>
|
||
<li><strong>Pattern Detection</strong>: Identifies common tool usage sequences and workflows</li>
|
||
<li><strong>Error Correlation</strong>: Finds tools that frequently fail together</li>
|
||
<li><strong>Session Analysis</strong>: Calculates productivity metrics and session duration</li>
|
||
<li><strong>Agent Comparison</strong>: Compares behavior across multiple agents/sessions</li>
|
||
<li><strong>Anomaly Detection</strong>: Identifies unusual patterns and outliers</li>
|
||
<li><strong>Real-time Progress</strong>: Shows analysis progress with percentage updates</li>
|
||
<li><strong>Sample Data Included</strong>: Test with realistic hook event scenarios</li>
|
||
</ul>
|
||
|
||
<h3>Web Research Integration</h3>
|
||
<p><strong>Source:</strong> <a href="https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Using_web_workers" target="_blank">MDN Web Workers API Documentation</a></p>
|
||
<p><strong>Techniques Applied:</strong></p>
|
||
<ul>
|
||
<li><strong>Worker Communication via postMessage</strong>: Main thread sends event data and receives analysis results through structured message passing</li>
|
||
<li><strong>Background Processing Pattern</strong>: Web Worker performs heavy computation (pattern matching, statistics) without blocking the UI thread</li>
|
||
<li><strong>Worker Lifecycle Management</strong>: Proper worker initialization, termination, and error handling for robust processing</li>
|
||
</ul>
|
||
|
||
<h3>Usage</h3>
|
||
<ol>
|
||
<li>Click "Load Sample Data" to test with example hook events, or upload your own JSON/JSONL file</li>
|
||
<li>Select an analysis type (Pattern Detection, Error Correlation, etc.)</li>
|
||
<li>Click "Start Analysis" to spawn a Web Worker and begin background processing</li>
|
||
<li>Watch the progress bar update in real-time as the worker processes events</li>
|
||
<li>Review results displayed in organized cards when analysis completes</li>
|
||
<li>Try different analysis types to explore various insights</li>
|
||
</ol>
|
||
|
||
<h3>Hook Event Structure</h3>
|
||
<p>Events follow this structure:</p>
|
||
<div class="sample-data">
|
||
<pre>{
|
||
"source_app": "demo-agent",
|
||
"session_id": "abc123",
|
||
"hook_event_type": "PreToolUse|PostToolUse|UserPromptSubmit|Notification|Stop|SubagentStop",
|
||
"payload": {
|
||
"tool_name": "Bash",
|
||
"tool_input": {...},
|
||
"tool_output": {...}
|
||
},
|
||
"timestamp": 1696867200000
|
||
}</pre>
|
||
</div>
|
||
|
||
<h3>Analysis Types</h3>
|
||
<ul>
|
||
<li><strong>Pattern Detection</strong>: Finds common tool sequences (e.g., "Read → Edit → Bash")</li>
|
||
<li><strong>Error Correlation</strong>: Identifies tools that fail together frequently</li>
|
||
<li><strong>Session Analysis</strong>: Calculates duration, tool counts, and productivity metrics</li>
|
||
<li><strong>Agent Comparison</strong>: Compares tool usage across different source apps/sessions</li>
|
||
<li><strong>Anomaly Detection</strong>: Finds outliers in timing, error rates, or usage patterns</li>
|
||
<li><strong>Complete Analysis</strong>: Runs all analyses in parallel for comprehensive insights</li>
|
||
</ul>
|
||
</section>
|
||
</main>
|
||
|
||
<footer>
|
||
<p>Claude Code DevTools | Generated via web-enhanced infinite loop</p>
|
||
<p>Web Source: <a href="https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Using_web_workers" target="_blank">MDN Web Workers API</a></p>
|
||
</footer>
|
||
|
||
<script>
|
||
// Global state
|
||
let eventData = [];
|
||
let worker = null;
|
||
|
||
// DOM elements
|
||
const eventFileInput = document.getElementById('eventFile');
|
||
const analysisTypeSelect = document.getElementById('analysisType');
|
||
const loadSampleBtn = document.getElementById('loadSampleBtn');
|
||
const analyzeBtn = document.getElementById('analyzeBtn');
|
||
const workerIndicator = document.getElementById('workerIndicator');
|
||
const workerStatus = document.getElementById('workerStatus');
|
||
const progressContainer = document.getElementById('progressContainer');
|
||
const progressBar = document.getElementById('progressBar');
|
||
const progressText = document.getElementById('progressText');
|
||
const resultsContainer = document.getElementById('resultsContainer');
|
||
|
||
// Sample data generator
|
||
function generateSampleData() {
|
||
const apps = ['web-app', 'api-server', 'cli-tool'];
|
||
const sessions = ['session-001', 'session-002', 'session-003'];
|
||
const tools = ['Bash', 'Read', 'Write', 'Edit', 'Grep', 'Glob', 'WebFetch'];
|
||
const eventTypes = ['PreToolUse', 'PostToolUse', 'UserPromptSubmit', 'Notification', 'Stop'];
|
||
|
||
const events = [];
|
||
const baseTime = Date.now() - 3600000; // 1 hour ago
|
||
|
||
for (let i = 0; i < 500; i++) {
|
||
const app = apps[Math.floor(Math.random() * apps.length)];
|
||
const session = sessions[Math.floor(Math.random() * sessions.length)];
|
||
const tool = tools[Math.floor(Math.random() * tools.length)];
|
||
const eventType = eventTypes[Math.floor(Math.random() * eventTypes.length)];
|
||
|
||
events.push({
|
||
source_app: app,
|
||
session_id: session,
|
||
hook_event_type: eventType,
|
||
payload: {
|
||
tool_name: tool,
|
||
tool_input: { command: `test-${i}` },
|
||
success: Math.random() > 0.1, // 10% error rate
|
||
duration_ms: Math.floor(Math.random() * 5000) + 100
|
||
},
|
||
timestamp: baseTime + (i * 7000) + Math.floor(Math.random() * 3000)
|
||
});
|
||
}
|
||
|
||
return events;
|
||
}
|
||
|
||
// Web Worker code as Blob (inline worker pattern)
|
||
function createWorkerCode() {
|
||
return `
|
||
// Worker message handler
|
||
self.onmessage = function(e) {
|
||
const { type, data, analysisType } = e.data;
|
||
|
||
if (type === 'analyze') {
|
||
try {
|
||
const results = performAnalysis(data, analysisType);
|
||
self.postMessage({ type: 'complete', results });
|
||
} catch (error) {
|
||
self.postMessage({ type: 'error', error: error.message });
|
||
}
|
||
}
|
||
};
|
||
|
||
function performAnalysis(events, analysisType) {
|
||
const results = {};
|
||
const total = events.length;
|
||
|
||
// Progress updates
|
||
function reportProgress(current, message) {
|
||
const percent = Math.round((current / total) * 100);
|
||
self.postMessage({
|
||
type: 'progress',
|
||
percent,
|
||
message
|
||
});
|
||
}
|
||
|
||
if (analysisType === 'patterns' || analysisType === 'all') {
|
||
reportProgress(total * 0.1, 'Detecting patterns...');
|
||
results.patterns = detectPatterns(events);
|
||
}
|
||
|
||
if (analysisType === 'errors' || analysisType === 'all') {
|
||
reportProgress(total * 0.3, 'Analyzing errors...');
|
||
results.errors = analyzeErrors(events);
|
||
}
|
||
|
||
if (analysisType === 'sessions' || analysisType === 'all') {
|
||
reportProgress(total * 0.5, 'Analyzing sessions...');
|
||
results.sessions = analyzeSessions(events);
|
||
}
|
||
|
||
if (analysisType === 'agents' || analysisType === 'all') {
|
||
reportProgress(total * 0.7, 'Comparing agents...');
|
||
results.agents = compareAgents(events);
|
||
}
|
||
|
||
if (analysisType === 'anomalies' || analysisType === 'all') {
|
||
reportProgress(total * 0.9, 'Detecting anomalies...');
|
||
results.anomalies = detectAnomalies(events);
|
||
}
|
||
|
||
reportProgress(total, 'Analysis complete');
|
||
return results;
|
||
}
|
||
|
||
function detectPatterns(events) {
|
||
const sequences = {};
|
||
const toolEvents = events.filter(e => e.payload?.tool_name);
|
||
|
||
for (let i = 0; i < toolEvents.length - 2; i++) {
|
||
const pattern = [
|
||
toolEvents[i].payload.tool_name,
|
||
toolEvents[i + 1].payload.tool_name,
|
||
toolEvents[i + 2].payload.tool_name
|
||
].join(' → ');
|
||
|
||
sequences[pattern] = (sequences[pattern] || 0) + 1;
|
||
}
|
||
|
||
return Object.entries(sequences)
|
||
.sort((a, b) => b[1] - a[1])
|
||
.slice(0, 10)
|
||
.map(([pattern, count]) => ({ pattern, count }));
|
||
}
|
||
|
||
function analyzeErrors(events) {
|
||
const errorPairs = {};
|
||
const failures = events.filter(e =>
|
||
e.payload?.success === false ||
|
||
e.hook_event_type === 'PostToolUse' && e.payload?.error
|
||
);
|
||
|
||
for (let i = 0; i < failures.length - 1; i++) {
|
||
const tool1 = failures[i].payload?.tool_name || 'unknown';
|
||
const tool2 = failures[i + 1].payload?.tool_name || 'unknown';
|
||
const pair = tool1 + ' → ' + tool2;
|
||
|
||
errorPairs[pair] = (errorPairs[pair] || 0) + 1;
|
||
}
|
||
|
||
return {
|
||
totalErrors: failures.length,
|
||
errorRate: ((failures.length / events.length) * 100).toFixed(1) + '%',
|
||
correlations: Object.entries(errorPairs)
|
||
.sort((a, b) => b[1] - a[1])
|
||
.slice(0, 5)
|
||
.map(([pair, count]) => ({ pair, count }))
|
||
};
|
||
}
|
||
|
||
function analyzeSessions(events) {
|
||
const sessions = {};
|
||
|
||
events.forEach(event => {
|
||
const sid = event.session_id;
|
||
if (!sessions[sid]) {
|
||
sessions[sid] = {
|
||
id: sid,
|
||
events: [],
|
||
tools: new Set(),
|
||
startTime: event.timestamp,
|
||
endTime: event.timestamp
|
||
};
|
||
}
|
||
|
||
sessions[sid].events.push(event);
|
||
sessions[sid].endTime = Math.max(sessions[sid].endTime, event.timestamp);
|
||
if (event.payload?.tool_name) {
|
||
sessions[sid].tools.add(event.payload.tool_name);
|
||
}
|
||
});
|
||
|
||
return Object.values(sessions).map(s => ({
|
||
id: s.id,
|
||
eventCount: s.events.length,
|
||
toolCount: s.tools.size,
|
||
duration: Math.round((s.endTime - s.startTime) / 1000) + 's',
|
||
durationMs: s.endTime - s.startTime
|
||
})).sort((a, b) => b.eventCount - a.eventCount);
|
||
}
|
||
|
||
function compareAgents(events) {
|
||
const agents = {};
|
||
|
||
events.forEach(event => {
|
||
const app = event.source_app;
|
||
if (!agents[app]) {
|
||
agents[app] = {
|
||
name: app,
|
||
eventCount: 0,
|
||
tools: {},
|
||
avgDuration: []
|
||
};
|
||
}
|
||
|
||
agents[app].eventCount++;
|
||
if (event.payload?.tool_name) {
|
||
const tool = event.payload.tool_name;
|
||
agents[app].tools[tool] = (agents[app].tools[tool] || 0) + 1;
|
||
}
|
||
if (event.payload?.duration_ms) {
|
||
agents[app].avgDuration.push(event.payload.duration_ms);
|
||
}
|
||
});
|
||
|
||
return Object.values(agents).map(a => {
|
||
const avgDur = a.avgDuration.length > 0
|
||
? Math.round(a.avgDuration.reduce((s, v) => s + v, 0) / a.avgDuration.length)
|
||
: 0;
|
||
|
||
const topTools = Object.entries(a.tools)
|
||
.sort((x, y) => y[1] - x[1])
|
||
.slice(0, 3)
|
||
.map(([name, count]) => name + '(' + count + ')')
|
||
.join(', ');
|
||
|
||
return {
|
||
name: a.name,
|
||
eventCount: a.eventCount,
|
||
topTools: topTools || 'N/A',
|
||
avgDuration: avgDur + 'ms'
|
||
};
|
||
}).sort((a, b) => b.eventCount - a.eventCount);
|
||
}
|
||
|
||
function detectAnomalies(events) {
|
||
const durations = events
|
||
.filter(e => e.payload?.duration_ms)
|
||
.map(e => e.payload.duration_ms);
|
||
|
||
if (durations.length === 0) {
|
||
return { outliers: [], stats: {} };
|
||
}
|
||
|
||
const avg = durations.reduce((s, v) => s + v, 0) / durations.length;
|
||
const variance = durations.reduce((s, v) => s + Math.pow(v - avg, 2), 0) / durations.length;
|
||
const stdDev = Math.sqrt(variance);
|
||
|
||
const outliers = events
|
||
.filter(e => e.payload?.duration_ms)
|
||
.filter(e => Math.abs(e.payload.duration_ms - avg) > 2 * stdDev)
|
||
.map(e => ({
|
||
tool: e.payload.tool_name || 'unknown',
|
||
duration: e.payload.duration_ms + 'ms',
|
||
deviation: ((e.payload.duration_ms - avg) / stdDev).toFixed(1) + 'σ'
|
||
}))
|
||
.slice(0, 5);
|
||
|
||
return {
|
||
outliers,
|
||
stats: {
|
||
mean: Math.round(avg) + 'ms',
|
||
stdDev: Math.round(stdDev) + 'ms',
|
||
total: durations.length
|
||
}
|
||
};
|
||
}
|
||
`;
|
||
}
|
||
|
||
// Initialize Web Worker
|
||
function initWorker() {
|
||
if (worker) {
|
||
worker.terminate();
|
||
}
|
||
|
||
const blob = new Blob([createWorkerCode()], { type: 'application/javascript' });
|
||
const workerUrl = URL.createObjectURL(blob);
|
||
worker = new Worker(workerUrl);
|
||
|
||
worker.onmessage = function(e) {
|
||
const { type, percent, message, results, error } = e.data;
|
||
|
||
if (type === 'progress') {
|
||
updateProgress(percent, message);
|
||
} else if (type === 'complete') {
|
||
displayResults(results);
|
||
updateWorkerStatus('complete', 'Analysis complete');
|
||
progressContainer.style.display = 'none';
|
||
analyzeBtn.disabled = false;
|
||
} else if (type === 'error') {
|
||
updateWorkerStatus('error', 'Error: ' + error);
|
||
progressContainer.style.display = 'none';
|
||
analyzeBtn.disabled = false;
|
||
}
|
||
};
|
||
|
||
worker.onerror = function(error) {
|
||
updateWorkerStatus('error', 'Worker error: ' + error.message);
|
||
progressContainer.style.display = 'none';
|
||
analyzeBtn.disabled = false;
|
||
};
|
||
|
||
updateWorkerStatus('idle', 'Worker ready');
|
||
}
|
||
|
||
// Update worker status indicator
|
||
function updateWorkerStatus(status, message) {
|
||
workerIndicator.className = 'status-indicator ' + status;
|
||
workerStatus.textContent = message;
|
||
}
|
||
|
||
// Update progress bar
|
||
function updateProgress(percent, message) {
|
||
progressBar.style.width = percent + '%';
|
||
progressBar.textContent = percent + '%';
|
||
progressText.textContent = message;
|
||
}
|
||
|
||
// Display analysis results
|
||
function displayResults(results) {
|
||
let html = '<div class="results-grid">';
|
||
|
||
if (results.patterns) {
|
||
html += `
|
||
<div class="result-card">
|
||
<h3>🔍 Common Patterns</h3>
|
||
<ul class="pattern-list">
|
||
${results.patterns.map(p => `
|
||
<li class="pattern-item">
|
||
<span class="pattern-sequence">${p.pattern}</span>
|
||
<span class="pattern-count">${p.count}x</span>
|
||
</li>
|
||
`).join('')}
|
||
</ul>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
if (results.errors) {
|
||
html += `
|
||
<div class="result-card">
|
||
<h3>❌ Error Analysis</h3>
|
||
<div class="result-item">
|
||
<div class="metric">
|
||
<span>Total Errors:</span>
|
||
<span class="metric-value">${results.errors.totalErrors}</span>
|
||
</div>
|
||
</div>
|
||
<div class="result-item">
|
||
<div class="metric">
|
||
<span>Error Rate:</span>
|
||
<span class="metric-value">${results.errors.errorRate}</span>
|
||
</div>
|
||
</div>
|
||
${results.errors.correlations.length > 0 ? `
|
||
<div style="margin-top: 0.75rem; font-size: 0.85rem; color: var(--text-secondary);">
|
||
Correlated Failures:
|
||
</div>
|
||
${results.errors.correlations.map(c => `
|
||
<div class="result-item">
|
||
<div class="metric">
|
||
<span style="font-size: 0.8rem;">${c.pair}</span>
|
||
<span class="metric-value">${c.count}x</span>
|
||
</div>
|
||
</div>
|
||
`).join('')}
|
||
` : ''}
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
if (results.sessions) {
|
||
html += `
|
||
<div class="result-card">
|
||
<h3>📊 Session Metrics</h3>
|
||
${results.sessions.slice(0, 5).map(s => `
|
||
<div class="result-item">
|
||
<div style="font-size: 0.75rem; color: var(--text-tertiary); margin-bottom: 0.25rem;">
|
||
${s.id}
|
||
</div>
|
||
<div class="metric">
|
||
<span>Events: ${s.eventCount}</span>
|
||
<span>Tools: ${s.toolCount}</span>
|
||
<span class="metric-value">${s.duration}</span>
|
||
</div>
|
||
</div>
|
||
`).join('')}
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
if (results.agents) {
|
||
html += `
|
||
<div class="result-card">
|
||
<h3>🤖 Agent Comparison</h3>
|
||
${results.agents.map(a => `
|
||
<div class="result-item">
|
||
<div style="font-weight: 600; margin-bottom: 0.25rem; color: var(--accent-blue);">
|
||
${a.name}
|
||
</div>
|
||
<div style="font-size: 0.8rem; color: var(--text-secondary);">
|
||
Events: ${a.eventCount} | Avg: ${a.avgDuration}
|
||
</div>
|
||
<div style="font-size: 0.75rem; color: var(--text-tertiary); margin-top: 0.25rem;">
|
||
${a.topTools}
|
||
</div>
|
||
</div>
|
||
`).join('')}
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
if (results.anomalies) {
|
||
html += `
|
||
<div class="result-card">
|
||
<h3>⚠️ Anomalies Detected</h3>
|
||
${results.anomalies.stats ? `
|
||
<div class="result-item">
|
||
<div class="metric">
|
||
<span>Mean Duration:</span>
|
||
<span class="metric-value">${results.anomalies.stats.mean}</span>
|
||
</div>
|
||
</div>
|
||
<div class="result-item">
|
||
<div class="metric">
|
||
<span>Std Deviation:</span>
|
||
<span class="metric-value">${results.anomalies.stats.stdDev}</span>
|
||
</div>
|
||
</div>
|
||
` : ''}
|
||
${results.anomalies.outliers.length > 0 ? `
|
||
<div style="margin-top: 0.75rem; font-size: 0.85rem; color: var(--text-secondary);">
|
||
Outliers (>2σ):
|
||
</div>
|
||
${results.anomalies.outliers.map(o => `
|
||
<div class="result-item">
|
||
<div class="metric">
|
||
<span style="font-size: 0.8rem;">${o.tool}</span>
|
||
<span>${o.duration}</span>
|
||
<span class="metric-value">${o.deviation}</span>
|
||
</div>
|
||
</div>
|
||
`).join('')}
|
||
` : '<div class="result-item">No outliers detected</div>'}
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
html += '</div>';
|
||
resultsContainer.innerHTML = html;
|
||
}
|
||
|
||
// File upload handler
|
||
eventFileInput.addEventListener('change', async (e) => {
|
||
const file = e.target.files[0];
|
||
if (!file) return;
|
||
|
||
try {
|
||
const text = await file.text();
|
||
|
||
if (file.name.endsWith('.jsonl')) {
|
||
eventData = text.trim().split('\n').map(line => JSON.parse(line));
|
||
} else {
|
||
eventData = JSON.parse(text);
|
||
if (!Array.isArray(eventData)) {
|
||
eventData = [eventData];
|
||
}
|
||
}
|
||
|
||
analyzeBtn.disabled = false;
|
||
updateWorkerStatus('idle', `Loaded ${eventData.length} events`);
|
||
} catch (error) {
|
||
alert('Error loading file: ' + error.message);
|
||
}
|
||
});
|
||
|
||
// Load sample data
|
||
loadSampleBtn.addEventListener('click', () => {
|
||
eventData = generateSampleData();
|
||
analyzeBtn.disabled = false;
|
||
updateWorkerStatus('idle', `Loaded ${eventData.length} sample events`);
|
||
});
|
||
|
||
// Start analysis
|
||
analyzeBtn.addEventListener('click', () => {
|
||
if (!worker) {
|
||
initWorker();
|
||
}
|
||
|
||
const analysisType = analysisTypeSelect.value;
|
||
analyzeBtn.disabled = true;
|
||
progressContainer.style.display = 'block';
|
||
updateProgress(0, 'Starting analysis...');
|
||
updateWorkerStatus('working', 'Processing events...');
|
||
|
||
// Send data to worker
|
||
worker.postMessage({
|
||
type: 'analyze',
|
||
data: eventData,
|
||
analysisType
|
||
});
|
||
});
|
||
|
||
// Initialize worker on load
|
||
if (window.Worker) {
|
||
initWorker();
|
||
} else {
|
||
updateWorkerStatus('error', 'Web Workers not supported');
|
||
analyzeBtn.disabled = true;
|
||
}
|
||
</script>
|
||
</body>
|
||
</html>
|