<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>RN Debugger Telemetry Dashboard</title>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
background: #0f172a;
color: #e2e8f0;
min-height: 100vh;
padding: 24px;
}
.header {
margin-bottom: 32px;
}
.header h1 {
font-size: 24px;
font-weight: 600;
color: #f8fafc;
margin-bottom: 8px;
}
.header p {
color: #94a3b8;
font-size: 14px;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 16px;
margin-bottom: 32px;
}
.stat-card {
background: #1e293b;
border-radius: 12px;
padding: 20px;
border: 1px solid #334155;
}
.stat-card .label {
font-size: 12px;
color: #94a3b8;
text-transform: uppercase;
letter-spacing: 0.5px;
margin-bottom: 8px;
}
.stat-card .value {
font-size: 32px;
font-weight: 700;
color: #f8fafc;
}
.stat-card .change {
font-size: 12px;
margin-top: 4px;
}
.stat-card .change.positive { color: #4ade80; }
.stat-card .change.negative { color: #f87171; }
.charts-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
gap: 24px;
margin-bottom: 32px;
}
.chart-card {
background: #1e293b;
border-radius: 12px;
padding: 24px;
border: 1px solid #334155;
}
.chart-card h3 {
font-size: 16px;
font-weight: 600;
margin-bottom: 16px;
color: #f8fafc;
}
.chart-container {
position: relative;
height: 300px;
}
.table-card {
background: #1e293b;
border-radius: 12px;
padding: 24px;
border: 1px solid #334155;
overflow-x: auto;
margin-bottom: 24px;
}
.table-card h3 {
font-size: 16px;
font-weight: 600;
margin-bottom: 16px;
color: #f8fafc;
}
table {
width: 100%;
border-collapse: collapse;
}
th, td {
text-align: left;
padding: 12px;
border-bottom: 1px solid #334155;
}
th {
color: #94a3b8;
font-size: 12px;
text-transform: uppercase;
letter-spacing: 0.5px;
font-weight: 500;
}
td {
font-size: 14px;
}
.status-badge {
display: inline-block;
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
font-weight: 500;
}
.status-badge.success {
background: rgba(74, 222, 128, 0.1);
color: #4ade80;
}
.status-badge.failure {
background: rgba(248, 113, 113, 0.1);
color: #f87171;
}
.loading {
text-align: center;
padding: 40px;
color: #94a3b8;
}
.error {
background: rgba(248, 113, 113, 0.1);
border: 1px solid #f87171;
border-radius: 8px;
padding: 16px;
color: #f87171;
margin-bottom: 24px;
}
.tool-tag {
display: inline-block;
background: #334155;
padding: 2px 8px;
border-radius: 4px;
font-size: 12px;
margin: 2px;
color: #e2e8f0;
}
code {
background: #334155;
padding: 2px 6px;
border-radius: 4px;
font-size: 12px;
color: #94a3b8;
}
.time-filter {
display: flex;
gap: 8px;
margin-bottom: 24px;
}
.time-filter button {
background: #334155;
border: none;
color: #e2e8f0;
padding: 8px 16px;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
transition: background 0.2s;
}
.time-filter button:hover {
background: #475569;
}
.time-filter button.active {
background: #3b82f6;
}
.refresh-btn {
background: #3b82f6;
border: none;
color: white;
padding: 8px 16px;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
margin-left: auto;
}
.refresh-btn:hover {
background: #2563eb;
}
.toggle-label {
display: flex;
align-items: center;
gap: 8px;
color: #e2e8f0;
font-size: 14px;
cursor: pointer;
padding: 8px 12px;
background: #334155;
border-radius: 6px;
}
.toggle-label input[type="checkbox"] {
width: 16px;
height: 16px;
cursor: pointer;
}
</style>
</head>
<body>
<div class="header">
<h1>RN Debugger Telemetry</h1>
<p>Usage analytics for React Native AI Debugger MCP Server</p>
</div>
<div class="time-filter">
<button data-days="1">24 Hours</button>
<button class="active" data-days="7">7 Days</button>
<button data-days="30">30 Days</button>
<label class="toggle-label">
<input type="checkbox" id="exclude-dev" checked onchange="loadData()">
<span>Exclude dev user</span>
</label>
<button class="refresh-btn" onclick="loadData()">Refresh</button>
<button class="refresh-btn" style="background: #64748b;" onclick="resetKey()">Reset Key</button>
</div>
<div id="error-container"></div>
<div class="stats-grid">
<div class="stat-card">
<div class="label">Sessions Started</div>
<div class="value" id="total-sessions">-</div>
<div class="change" id="sessions-detail"></div>
</div>
<div class="stat-card">
<div class="label">Unique Installations</div>
<div class="value" id="unique-installs">-</div>
</div>
<div class="stat-card">
<div class="label">Total Tool Calls</div>
<div class="value" id="total-calls">-</div>
</div>
<div class="stat-card">
<div class="label">Active Users</div>
<div class="value" id="active-users">-</div>
<div class="change" id="active-users-detail"></div>
</div>
<div class="stat-card">
<div class="label">Inactive Users</div>
<div class="value" id="inactive-users">-</div>
<div class="change" id="inactive-users-detail"></div>
</div>
<div class="stat-card">
<div class="label">Success Rate</div>
<div class="value" id="success-rate">-</div>
</div>
<div class="stat-card">
<div class="label">Avg Duration</div>
<div class="value" id="avg-duration">-</div>
</div>
</div>
<div class="charts-grid">
<div class="chart-card">
<h3>Tool Usage</h3>
<div class="chart-container">
<canvas id="toolUsageChart"></canvas>
</div>
</div>
<div class="chart-card">
<h3>Calls Over Time</h3>
<div class="chart-container">
<canvas id="timelineChart"></canvas>
</div>
</div>
<div class="chart-card">
<h3>User Activity</h3>
<div class="chart-container">
<canvas id="userActivityChart"></canvas>
</div>
</div>
</div>
<div class="table-card">
<h3>Top Tools by Usage</h3>
<table>
<thead>
<tr>
<th>Tool Name</th>
<th>Calls</th>
<th>Success Rate</th>
<th>Avg Duration</th>
</tr>
</thead>
<tbody id="tools-table">
<tr><td colspan="4" class="loading">Loading...</td></tr>
</tbody>
</table>
</div>
<div class="table-card">
<h3>Users Activity (5+ calls/week = Active)</h3>
<table>
<thead>
<tr>
<th>User ID</th>
<th>Total Calls</th>
<th>Calls/Week</th>
<th>Status</th>
</tr>
</thead>
<tbody id="users-activity-table">
<tr><td colspan="4" class="loading">Loading...</td></tr>
</tbody>
</table>
</div>
<div class="table-card">
<h3>Tools Usage by User</h3>
<table>
<thead>
<tr>
<th>User ID</th>
<th>Total Calls</th>
<th>Top Tools</th>
</tr>
</thead>
<tbody id="user-tools-table">
<tr><td colspan="3" class="loading">Loading...</td></tr>
</tbody>
</table>
</div>
<script>
// Configuration
const API_URL = 'https://rn-debugger-telemetry.500griven.workers.dev';
let toolUsageChart = null;
let timelineChart = null;
let userActivityChart = null;
let selectedDays = 7;
let dashboardKey = localStorage.getItem('dashboardKey') || '';
// Prompt for key if not set
if (!dashboardKey) {
dashboardKey = prompt('Enter your Dashboard Key:');
if (dashboardKey) {
localStorage.setItem('dashboardKey', dashboardKey);
}
}
// Time filter buttons
document.querySelectorAll('.time-filter button[data-days]').forEach(btn => {
btn.addEventListener('click', () => {
document.querySelectorAll('.time-filter button[data-days]').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
selectedDays = parseInt(btn.dataset.days);
loadData();
});
});
async function loadData() {
if (!dashboardKey) {
document.getElementById('error-container').innerHTML = `
<div class="error">No dashboard key provided. Refresh the page to enter your key.</div>
`;
return;
}
try {
const excludeDev = document.getElementById('exclude-dev').checked;
const response = await fetch(`${API_URL}/api/stats?days=${selectedDays}&key=${encodeURIComponent(dashboardKey)}${excludeDev ? '&excludeDev=1' : ''}`);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${await response.text()}`);
}
const data = await response.json();
updateDashboard(data);
document.getElementById('error-container').innerHTML = '';
} catch (error) {
console.error('Failed to load data:', error);
document.getElementById('error-container').innerHTML = `
<div class="error">
Failed to load data: ${error.message}<br>
<small>Make sure the worker has the /api/stats endpoint deployed.</small>
</div>
`;
}
}
function updateDashboard(data) {
// Update stat cards
const totalSessions = data.totalSessions || 0;
const uniqueInstalls = data.uniqueInstalls || 0;
document.getElementById('total-sessions').textContent = totalSessions.toLocaleString();
document.getElementById('unique-installs').textContent = uniqueInstalls.toLocaleString();
document.getElementById('total-calls').textContent = (data.totalCalls || 0).toLocaleString();
document.getElementById('success-rate').textContent = `${(data.successRate || 0).toFixed(1)}%`;
document.getElementById('avg-duration').textContent = `${(data.avgDuration || 0).toFixed(0)}ms`;
// Calculate sessions per user
if (uniqueInstalls > 0) {
const sessionsPerUser = (totalSessions / uniqueInstalls).toFixed(1);
document.getElementById('sessions-detail').textContent = `${sessionsPerUser} per user`;
document.getElementById('sessions-detail').className = 'change';
}
// Update active/inactive user stats
const userActivity = data.userActivity || { activeUsers: 0, inactiveUsers: 0 };
document.getElementById('active-users').textContent = userActivity.activeUsers.toLocaleString();
document.getElementById('inactive-users').textContent = userActivity.inactiveUsers.toLocaleString();
document.getElementById('active-users-detail').textContent = `5+ calls/week`;
document.getElementById('active-users-detail').className = 'change positive';
document.getElementById('inactive-users-detail').textContent = `<5 calls/week`;
document.getElementById('inactive-users-detail').className = 'change negative';
// Update tool usage chart
updateToolUsageChart(data.toolBreakdown || []);
// Update timeline chart
updateTimelineChart(data.timeline || []);
// Update user activity pie chart
updateUserActivityChart(userActivity);
// Update tools table
updateToolsTable(data.toolBreakdown || []);
// Update users activity table
updateUsersActivityTable(userActivity.users || []);
// Update user tools table
updateUserToolsTable(data.userToolsBreakdown || []);
}
function updateToolUsageChart(toolData) {
const ctx = document.getElementById('toolUsageChart').getContext('2d');
const labels = toolData.slice(0, 10).map(t => t.tool);
const values = toolData.slice(0, 10).map(t => t.count);
if (toolUsageChart) {
toolUsageChart.destroy();
}
toolUsageChart = new Chart(ctx, {
type: 'bar',
data: {
labels,
datasets: [{
data: values,
backgroundColor: '#3b82f6',
borderRadius: 4
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: { display: false }
},
scales: {
x: {
ticks: { color: '#94a3b8' },
grid: { color: '#334155' }
},
y: {
ticks: { color: '#94a3b8' },
grid: { color: '#334155' }
}
}
}
});
}
function updateTimelineChart(timelineData) {
const ctx = document.getElementById('timelineChart').getContext('2d');
const labels = timelineData.map(t => t.date);
const values = timelineData.map(t => t.count);
if (timelineChart) {
timelineChart.destroy();
}
timelineChart = new Chart(ctx, {
type: 'line',
data: {
labels,
datasets: [{
data: values,
borderColor: '#3b82f6',
backgroundColor: 'rgba(59, 130, 246, 0.1)',
fill: true,
tension: 0.3
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: { display: false }
},
scales: {
x: {
ticks: { color: '#94a3b8' },
grid: { color: '#334155' }
},
y: {
ticks: { color: '#94a3b8' },
grid: { color: '#334155' }
}
}
}
});
}
function updateToolsTable(toolData) {
const tbody = document.getElementById('tools-table');
if (toolData.length === 0) {
tbody.innerHTML = '<tr><td colspan="4" class="loading">No data available</td></tr>';
return;
}
tbody.innerHTML = toolData.map(tool => `
<tr>
<td>${tool.tool}</td>
<td>${tool.count.toLocaleString()}</td>
<td>
<span class="status-badge ${tool.successRate >= 90 ? 'success' : 'failure'}">
${tool.successRate.toFixed(1)}%
</span>
</td>
<td>${tool.avgDuration.toFixed(0)}ms</td>
</tr>
`).join('');
}
function updateUserActivityChart(activityData) {
const ctx = document.getElementById('userActivityChart').getContext('2d');
if (userActivityChart) {
userActivityChart.destroy();
}
const active = activityData.activeUsers || 0;
const inactive = activityData.inactiveUsers || 0;
if (active === 0 && inactive === 0) {
ctx.font = '14px sans-serif';
ctx.fillStyle = '#94a3b8';
ctx.textAlign = 'center';
ctx.fillText('No data available', ctx.canvas.width / 2, ctx.canvas.height / 2);
return;
}
userActivityChart = new Chart(ctx, {
type: 'doughnut',
data: {
labels: ['Active Users', 'Inactive Users'],
datasets: [{
data: [active, inactive],
backgroundColor: ['#4ade80', '#f87171'],
borderWidth: 0
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
position: 'bottom',
labels: { color: '#94a3b8' }
}
}
}
});
}
function updateUsersActivityTable(users) {
const tbody = document.getElementById('users-activity-table');
if (users.length === 0) {
tbody.innerHTML = '<tr><td colspan="4" class="loading">No data available</td></tr>';
return;
}
tbody.innerHTML = users.map(user => `
<tr>
<td><code>${user.userId}</code></td>
<td>${user.totalCalls.toLocaleString()}</td>
<td>${user.callsPerWeek}</td>
<td>
<span class="status-badge ${user.isActive ? 'success' : 'failure'}">
${user.isActive ? 'Active' : 'Inactive'}
</span>
</td>
</tr>
`).join('');
}
function updateUserToolsTable(userToolsData) {
const tbody = document.getElementById('user-tools-table');
if (userToolsData.length === 0) {
tbody.innerHTML = '<tr><td colspan="3" class="loading">No data available</td></tr>';
return;
}
tbody.innerHTML = userToolsData.map(user => {
const topTools = user.tools.slice(0, 5).map(t =>
`<span class="tool-tag">${t.tool} (${t.count})</span>`
).join(' ');
return `
<tr>
<td><code>${user.userId}</code></td>
<td>${user.totalCalls.toLocaleString()}</td>
<td>${topTools}</td>
</tr>
`;
}).join('');
}
function formatDate(dateStr) {
if (!dateStr) return '-';
try {
const date = new Date(dateStr);
return date.toLocaleDateString() + ' ' + date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
} catch {
return dateStr;
}
}
function resetKey() {
localStorage.removeItem('dashboardKey');
dashboardKey = prompt('Enter your Dashboard Key:');
if (dashboardKey) {
localStorage.setItem('dashboardKey', dashboardKey);
loadData();
}
}
// Initial load
loadData();
</script>
</body>
</html>