UNPKG

gemini-cli-templates

Version:

Advanced analytics dashboard and monitoring tool for Gemini CLI with real-time metrics, token tracking, and telemetry visualization

636 lines (552 loc) 21.5 kB
let charts = {}; let currentDateRange = { days: 7 }; let currentMetricsPage = 1; let currentTracesPage = 1; const itemsPerPage = 20; export function updateTimestamp() { document.getElementById('timestamp').textContent = new Date().toLocaleTimeString(); } export function toggleTheme() { const body = document.body; const themeToggle = document.getElementById('theme-toggle'); if (body.getAttribute('data-theme') === 'light') { body.removeAttribute('data-theme'); themeToggle.innerHTML = '🌙 Dark'; localStorage.setItem('theme', 'dark'); } else { body.setAttribute('data-theme', 'light'); themeToggle.innerHTML = '☀️ Light'; localStorage.setItem('theme', 'light'); } } export function loadTheme() { const savedTheme = localStorage.getItem('theme'); const themeToggle = document.getElementById('theme-toggle'); if (savedTheme === 'light') { document.body.setAttribute('data-theme', 'light'); themeToggle.innerHTML = '☀️ Light'; } else { themeToggle.innerHTML = '🌙 Dark'; } } export function showTab(tabName) { document.querySelectorAll('.tab-content').forEach(content => { content.classList.remove('active'); }); document.querySelectorAll('.tab-button').forEach(button => { button.classList.remove('active'); }); document.getElementById(tabName).classList.add('active'); document.querySelector(`.tab-button[data-tab="${tabName}"]`).classList.add('active'); } export function initializeDateFilters() { const presets = document.querySelectorAll('.filter-preset'); const customInputs = document.getElementById('custom-date-inputs'); const startDateInput = document.getElementById('start-date'); const endDateInput = document.getElementById('end-date'); // Set default date range (last 7 days) const endDate = new Date(); const startDate = new Date(); startDate.setDate(startDate.getDate() - 7); startDateInput.value = startDate.toISOString().slice(0, 16); endDateInput.value = endDate.toISOString().slice(0, 16); presets.forEach(preset => { preset.addEventListener('click', () => { presets.forEach(p => p.classList.remove('active')); preset.classList.add('active'); const range = preset.dataset.range; if (range === 'custom') { customInputs.style.display = 'flex'; } else { customInputs.style.display = 'none'; currentDateRange = { days: parseInt(range) }; updateAnalytics(); } }); }); document.getElementById('apply-filter').addEventListener('click', () => { const start = new Date(startDateInput.value); const end = new Date(endDateInput.value); currentDateRange = { start, end }; updateAnalytics(); }); } export function updateMainMetrics(metrics, traces) { const sessionIds = [...new Set(metrics.map(m => m.sessionId))]; const totalSessions = sessionIds.length; const toolMetrics = metrics.filter(m => m.name && (m.name.includes('tool') || m.attributes?.operation)); const totalTools = toolMetrics.reduce((sum, m) => sum + m.value, 0); const tokenMetrics = metrics.filter(m => m.name && m.name.includes('token')); const inputTokens = tokenMetrics.filter(m => m.attributes?.type === 'input').reduce((sum, m) => sum + m.value, 0); const outputTokens = tokenMetrics.filter(m => m.attributes?.type === 'output').reduce((sum, m) => sum + m.value, 0); const cachedTokens = tokenMetrics.filter(m => m.attributes?.type === 'cached').reduce((sum, m) => sum + m.value, 0); const thoughtsTokens = tokenMetrics.filter(m => m.attributes?.type === 'thoughts').reduce((sum, m) => sum + m.value, 0); const totalTokens = inputTokens + outputTokens + cachedTokens + thoughtsTokens; const apiCalls = traces.reduce((sum, trace) => sum + trace.spans.filter(s => s.operationName.includes('gemini.api')).length, 0); const avgDuration = traces.length > 0 ? Math.round(traces.reduce((sum, trace) => { const apiSpans = trace.spans.filter(s => s.operationName.includes('gemini.api')); return sum + apiSpans.reduce((spanSum, span) => spanSum + span.duration, 0) / (apiSpans.length || 1); }, 0) / traces.length / 1000) : 0; document.getElementById('total-sessions').textContent = totalSessions; document.getElementById('sessions-week').textContent = totalSessions; document.getElementById('sessions-active').textContent = sessionIds.length > 0 ? '1' : '0'; document.getElementById('total-tools').textContent = totalTools.toLocaleString(); document.getElementById('tools-week').textContent = totalTools.toLocaleString(); document.getElementById('tools-success').textContent = '100%'; document.getElementById('total-tokens').textContent = totalTokens.toLocaleString(); document.getElementById('tokens-input').textContent = inputTokens.toLocaleString(); document.getElementById('tokens-output').textContent = outputTokens.toLocaleString(); // Update cached and thoughts tokens if elements exist const cachedElement = document.getElementById('tokens-cached'); const thoughtsElement = document.getElementById('tokens-thoughts'); if (cachedElement) cachedElement.textContent = cachedTokens.toLocaleString(); if (thoughtsElement) thoughtsElement.textContent = thoughtsTokens.toLocaleString(); document.getElementById('total-apis').textContent = apiCalls.toLocaleString(); document.getElementById('api-duration').textContent = `${avgDuration}ms`; document.getElementById('api-success').textContent = '100%'; if (sessionIds.length > 0) { const currentSession = document.getElementById('current-session'); currentSession.style.display = 'block'; document.getElementById('session-detail').textContent = `Session ID: ${sessionIds[0].substring(0, 8)}...`; } } export function renderAnalyticsCharts(metrics, traces) { renderTokenChart(metrics); renderSessionsChart(metrics); renderResponseTimesChart(traces, metrics); renderToolsChart(metrics); // Update chart summaries const tokenMetrics = metrics.filter(m => m.name && m.name.includes('token')); const totalTokens = tokenMetrics.reduce((sum, m) => sum + m.value, 0); document.getElementById('total-tokens-chart').textContent = totalTokens.toLocaleString(); const sessionIds = [...new Set(metrics.map(m => m.sessionId))]; document.getElementById('total-sessions-chart').textContent = sessionIds.length; const toolMetrics = metrics.filter(m => m.name && (m.name.includes('tool') || m.attributes?.operation)); const totalTools = toolMetrics.reduce((sum, m) => sum + m.value, 0); document.getElementById('total-tools-chart').textContent = totalTools.toLocaleString(); // Calculate average response time from actual metrics const responseTimeMetrics = metrics.filter(m => m.name && m.name.includes('response_time')); const avgResponseTime = responseTimeMetrics.length > 0 ? Math.round(responseTimeMetrics.reduce((sum, m) => sum + m.value, 0) / responseTimeMetrics.length) : 0; document.getElementById('avg-response-time').textContent = `${avgResponseTime}ms`; } export function renderTokenChart(metrics) { const tokenMetrics = metrics.filter(m => m.name && m.name.includes('token')); const inputTokens = tokenMetrics.filter(m => m.attributes?.type === 'input').reduce((sum, m) => sum + m.value, 0); const outputTokens = tokenMetrics.filter(m => m.attributes?.type === 'output').reduce((sum, m) => sum + m.value, 0); const cachedTokens = tokenMetrics.filter(m => m.attributes?.type === 'cached').reduce((sum, m) => sum + m.value, 0); const thoughtsTokens = tokenMetrics.filter(m => m.attributes?.type === 'thoughts').reduce((sum, m) => sum + m.value, 0); const ctx = document.getElementById('token-chart').getContext('2d'); if (charts.tokenChart) { charts.tokenChart.destroy(); } const chartData = []; const chartLabels = []; const backgroundColors = []; const borderColors = []; if (inputTokens > 0) { chartData.push(inputTokens); chartLabels.push('Input Tokens'); backgroundColors.push('rgba(255, 99, 132, 0.8)'); borderColors.push('rgba(255, 99, 132, 1)'); } if (outputTokens > 0) { chartData.push(outputTokens); chartLabels.push('Output Tokens'); backgroundColors.push('rgba(54, 162, 235, 0.8)'); borderColors.push('rgba(54, 162, 235, 1)'); } if (cachedTokens > 0) { chartData.push(cachedTokens); chartLabels.push('Cached Tokens'); backgroundColors.push('rgba(75, 192, 192, 0.8)'); borderColors.push('rgba(75, 192, 192, 1)'); } if (thoughtsTokens > 0) { chartData.push(thoughtsTokens); chartLabels.push('Thoughts Tokens'); backgroundColors.push('rgba(255, 206, 86, 0.8)'); borderColors.push('rgba(255, 206, 86, 1)'); } charts.tokenChart = new Chart(ctx, { type: 'doughnut', data: { labels: chartLabels, datasets: [{ label: 'Token Usage', data: chartData, backgroundColor: backgroundColors, borderColor: borderColors, borderWidth: 2 }] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { position: 'bottom', labels: { color: '#ffffff', font: { family: 'Monaco, monospace', size: 11 } } } } } }); } export function renderSessionsChart(metrics) { const ctx = document.getElementById('sessions-chart').getContext('2d'); if (charts.sessionsChart) { charts.sessionsChart.destroy(); } // Group metrics by date const sessionsByDate = {}; metrics.forEach(metric => { const date = new Date(metric.timestamp).toDateString(); if (!sessionsByDate[date]) { sessionsByDate[date] = new Set(); } sessionsByDate[date].add(metric.sessionId); }); const dates = Object.keys(sessionsByDate).slice(-7); const sessionCounts = dates.map(date => sessionsByDate[date].size); charts.sessionsChart = new Chart(ctx, { type: 'bar', data: { labels: dates, datasets: [{ label: 'Sessions', data: sessionCounts, backgroundColor: 'rgba(0, 206, 209, 0.8)', borderColor: 'rgba(0, 206, 209, 1)', borderWidth: 2 }] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { labels: { color: '#ffffff', font: { family: 'Monaco, monospace', size: 11 } } } }, scales: { y: { beginAtZero: true, ticks: { color: '#ffffff' }, grid: { color: '#333333' } }, x: { ticks: { color: '#ffffff' }, grid: { color: '#333333' } } } } }); } export function renderResponseTimesChart(traces, metrics = []) { const ctx = document.getElementById('response-times-chart').getContext('2d'); if (charts.responseChart) { charts.responseChart.destroy(); } // Use actual API response time metrics if available, otherwise fallback to traces const responseTimeMetrics = metrics.filter(m => m.name && m.name.includes('response_time')); let responseTimes = []; if (responseTimeMetrics.length > 0) { // Use actual API response time data from metrics responseTimes = responseTimeMetrics .slice(-20) .sort((a, b) => new Date(a.timestamp) - new Date(b.timestamp)) .map((metric, index) => ({ x: index + 1, y: metric.value, timestamp: metric.timestamp })); } else if (traces && traces.length > 0) { // Fallback to trace data responseTimes = traces.slice(-20).map((trace, index) => { if (trace.spans && trace.spans.length > 0) { const maxDuration = Math.max(...trace.spans.map(s => s.duration || 0)); return { x: index + 1, y: maxDuration / 1000 }; } return { x: index + 1, y: 0 }; }); } else { // No data available - show placeholder responseTimes = [ {x: 1, y: 0}, {x: 2, y: 0}, {x: 3, y: 0} ]; } charts.responseChart = new Chart(ctx, { type: 'line', data: { datasets: [{ label: 'Response Time (ms)', data: responseTimes, borderColor: 'rgba(255, 20, 147, 1)', backgroundColor: 'rgba(255, 20, 147, 0.2)', tension: 0.4, fill: true }] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { labels: { color: '#ffffff', font: { family: 'Monaco, monospace', size: 11 } } } }, scales: { y: { beginAtZero: true, ticks: { color: '#ffffff' }, grid: { color: '#333333' } }, x: { ticks: { color: '#ffffff' }, grid: { color: '#333333' } } } } }); } export function renderToolsChart(metrics) { const ctx = document.getElementById('tools-chart').getContext('2d'); if (charts.toolsChart) { charts.toolsChart.destroy(); } const toolMetrics = metrics.filter(m => m.name && m.name.includes('tool')); const toolsByType = {}; toolMetrics.forEach(metric => { const operation = metric.attributes?.operation || 'unknown'; toolsByType[operation] = (toolsByType[operation] || 0) + metric.value; }); const labels = Object.keys(toolsByType); const data = Object.values(toolsByType); const colors = [ 'rgba(30, 144, 255, 0.8)', 'rgba(138, 43, 226, 0.8)', 'rgba(255, 20, 147, 0.8)', 'rgba(0, 206, 209, 0.8)', 'rgba(255, 206, 86, 0.8)' ]; charts.toolsChart = new Chart(ctx, { type: 'bar', data: { labels: labels, datasets: [{ label: 'Tool Usage', data: data, backgroundColor: colors.slice(0, labels.length), borderColor: colors.slice(0, labels.length).map(c => c.replace('0.8', '1')), borderWidth: 2 }] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { labels: { color: '#ffffff', font: { family: 'Monaco, monospace', size: 11 } } } }, scales: { y: { beginAtZero: true, ticks: { color: '#ffffff' }, grid: { color: '#333333' } }, x: { ticks: { color: '#ffffff' }, grid: { color: '#333333' } } } } }); } export function renderMetricsLogs(data, filter = 'all', page = 1) { const metricsDiv = document.getElementById('metrics-logs-content'); const metrics = data.metrics || []; let filteredMetrics = metrics; if (filter !== 'all') { filteredMetrics = metrics.filter(m => m.name && m.name.includes(filter)); } const totalPages = Math.ceil(filteredMetrics.length / itemsPerPage); const startIndex = (page - 1) * itemsPerPage; const endIndex = startIndex + itemsPerPage; const pageMetrics = filteredMetrics.slice(startIndex, endIndex); if (pageMetrics.length === 0) { metricsDiv.innerHTML = ` <div class="empty-state"> <h3>📋 No metrics found</h3> <p>No metrics match the current filter criteria.</p> </div> `; return; } let html = ''; pageMetrics.forEach(metric => { const badges = []; if (metric.name.includes('token')) badges.push('<span class="log-badge token">Token</span>'); if (metric.name.includes('session')) badges.push('<span class="log-badge session">Session</span>'); if (metric.name.includes('tool')) badges.push('<span class="log-badge tool">Tool</span>'); html += ` <div class="log-item"> <div class="log-item-header"> <div class="log-item-title">${metric.name}</div> <div class="log-item-timestamp">${new Date(metric.timestamp).toLocaleString()}</div> </div> <div class="log-item-details"> <div class="log-detail"> <span class="log-detail-label">Value:</span> <span class="log-detail-value">${metric.value.toLocaleString()}</span> </div> <div class="log-detail"> <span class="log-detail-label">Session:</span> <span class="log-detail-value">${metric.sessionId.substring(0, 8)}...</span> </div> ${Object.entries(metric.attributes || {}).map(([key, value]) => ` <div class="log-detail"> <span class="log-detail-label">${key}:</span> <span class="log-detail-value">${value}</span> </div> `).join('')} </div> <div class="log-badges">${badges.join('')}</div> </div> `; }); metricsDiv.innerHTML = html; // Update pagination updatePagination('metrics', page, totalPages); // Update stats document.getElementById('metrics-count').textContent = `${filteredMetrics.length} metrics`; document.getElementById('metrics-timestamp').textContent = new Date().toLocaleTimeString(); } export function renderTracesLogs(traces, filter = 'all', page = 1) { const tracesDiv = document.getElementById('traces-logs-content'); let filteredTraces = traces; if (filter !== 'all') { filteredTraces = traces.filter(trace => trace.spans.some(span => span.operationName.includes(filter)) ); } const totalPages = Math.ceil(filteredTraces.length / itemsPerPage); const startIndex = (page - 1) * itemsPerPage; const endIndex = startIndex + itemsPerPage; const pageTraces = filteredTraces.slice(startIndex, endIndex); if (pageTraces.length === 0) { tracesDiv.innerHTML = ` <div class="empty-state"> <h3>🔍 No traces found</h3> <p>No traces match the current filter criteria.</p> </div> `; return; } let html = ''; pageTraces.forEach(trace => { const badges = []; if (trace.spans.some(s => s.operationName.includes('api'))) badges.push('<span class="log-badge api">API</span>'); if (trace.spans.some(s => s.operationName.includes('tool'))) badges.push('<span class="log-badge tool">Tool</span>'); const totalDuration = Math.max(...trace.spans.map(s => s.duration)) / 1000; html += ` <div class="log-item"> <div class="log-item-header"> <div class="log-item-title">Trace ${trace.traceID.substring(0, 16)}...</div> <div class="log-item-timestamp">${totalDuration.toFixed(2)}ms</div> </div> <div class="log-item-details"> <div class="log-detail"> <span class="log-detail-label">Spans:</span> <span class="log-detail-value">${trace.spans.length}</span> </div> <div class="log-detail"> <span class="log-detail-label">Operations:</span> <span class="log-detail-value">${[...new Set(trace.spans.map(s => s.operationName))].length}</span> </div> </div> <div class="log-badges">${badges.join('')}</div> <div style="margin-top: 12px;"> ${trace.spans.slice(0, 3).map(span => ` <div style="font-size: 0.8em; color: var(--dim-text); margin: 4px 0;"> ${span.operationName} - ${(span.duration / 1000).toFixed(2)}ms </div> `).join('')} ${trace.spans.length > 3 ? `<div style="font-size: 0.8em; color: var(--dim-text);">... and ${trace.spans.length - 3} more spans</div>` : ''} </div> </div> `; }); tracesDiv.innerHTML = html; // Update pagination updatePagination('traces', page, totalPages); // Update stats document.getElementById('traces-count').textContent = `${filteredTraces.length} traces`; document.getElementById('traces-timestamp').textContent = new Date().toLocaleTimeString(); } function updatePagination(type, currentPage, totalPages) { const prevBtn = document.getElementById(`${type}-prev`); const nextBtn = document.getElementById(`${type}-next`); const pageInfo = document.getElementById(`${type}-page-info`); prevBtn.disabled = currentPage <= 1; nextBtn.disabled = currentPage >= totalPages; pageInfo.textContent = `Page ${currentPage} of ${totalPages}`; } export function setupPagination() { document.getElementById('metrics-prev').addEventListener('click', () => { if (currentMetricsPage > 1) { currentMetricsPage--; loadMetricsLogs(); } }); document.getElementById('metrics-next').addEventListener('click', () => { currentMetricsPage++; loadMetricsLogs(); }); document.getElementById('traces-prev').addEventListener('click', () => { if (currentTracesPage > 1) { currentTracesPage--; loadTracesLogs(); } }); document.getElementById('traces-next').addEventListener('click', () => { currentTracesPage++; loadTracesLogs(); }); document.getElementById('metrics-filter').addEventListener('change', (e) => { currentMetricsPage = 1; loadMetricsLogs(); }); document.getElementById('traces-filter').addEventListener('change', (e) => { currentTracesPage = 1; loadTracesLogs(); }); } // Export functions that will be called from app.js window.updateAnalytics = async function() { // This will be implemented in app.js }; window.loadMetricsLogs = async function() { // This will be implemented in app.js }; window.loadTracesLogs = async function() { // This will be implemented in app.js };