Skip to content

Commit 20ec7c9

Browse files
author
root
committed
Refine telemetry: 30-day window, latest-record deduplication, and activity trend chart
1 parent 851ce07 commit 20ec7c9

File tree

1 file changed

+98
-19
lines changed

1 file changed

+98
-19
lines changed

src/index.js

Lines changed: 98 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ export default {
9898
});
9999
}
100100

101-
// Query 1: Active instances and their latest properties (Last 7 Days)
101+
// Query 1: Active instances and their latest properties (Last 30 Days)
102102
const sqlActive = `
103103
SELECT
104104
blob1 as instance_id,
@@ -107,22 +107,33 @@ export default {
107107
blob4 as arch,
108108
blob6 as country,
109109
blob5 as cpu,
110-
max(double1) as cpu_cores,
111-
max(double2) as ram,
112-
max(double3) as cameras,
113-
max(double4) as groups,
114-
max(double5) as events,
115-
max(double6) as gpu,
116-
max(double7) as notifications
110+
timestamp,
111+
double1 as cpu_cores,
112+
double2 as ram,
113+
double3 as cameras,
114+
double4 as groups,
115+
double5 as events,
116+
double6 as gpu,
117+
double7 as notifications
117118
FROM vibenvr_telemetry_events
118-
WHERE timestamp >= NOW() - INTERVAL '7' DAY
119-
GROUP BY blob1, blob2, blob3, blob4, blob6, blob5
119+
WHERE timestamp >= NOW() - INTERVAL '30' DAY
120120
`;
121121

122122
const sqlTotal = `SELECT count(DISTINCT blob1) as total FROM vibenvr_telemetry_events`;
123123

124+
const sqlActivity = `
125+
SELECT
126+
toStartOfDay(timestamp) as day,
127+
count() as pings,
128+
count(DISTINCT blob1) as uniques
129+
FROM vibenvr_telemetry_events
130+
WHERE timestamp >= NOW() - INTERVAL '30' DAY
131+
GROUP BY day
132+
ORDER BY day ASC
133+
`;
134+
124135
try {
125-
const [resActive, resTotal] = await Promise.all([
136+
const [resActive, resTotal, resActivity] = await Promise.all([
126137
fetch(`https://api.cloudflare.com/client/v4/accounts/${env.ACCOUNT_ID}/analytics_engine/sql`, {
127138
method: 'POST',
128139
headers: { 'Authorization': `Bearer ${env.API_TOKEN}` },
@@ -132,31 +143,35 @@ export default {
132143
method: 'POST',
133144
headers: { 'Authorization': `Bearer ${env.API_TOKEN}` },
134145
body: sqlTotal
146+
}),
147+
fetch(`https://api.cloudflare.com/client/v4/accounts/${env.ACCOUNT_ID}/analytics_engine/sql`, {
148+
method: 'POST',
149+
headers: { 'Authorization': `Bearer ${env.API_TOKEN}` },
150+
body: sqlActivity
135151
})
136152
]);
137153

138154
const activeStr = await resActive.text();
139155
const totalStr = await resTotal.text();
156+
const activityStr = await resActivity.text();
140157

141158
if (!resActive.ok) throw new Error("SQL API Error: " + activeStr);
142159

143160
const activeData = JSON.parse(activeStr).data || [];
144161
const totalData = JSON.parse(totalStr).data || [];
162+
const activityData = JSON.parse(activityStr).data || [];
145163

146164
let activeCount = activeData.length;
147165
const totalCount = parseInt(totalData[0]?.total || "0", 10);
148166

149-
// Deduplicate instances: pick the best record for each ID
167+
// Deduplicate instances: pick the latest record for each ID based on timestamp
150168
const uniqueInstances = new Map();
151169
for (const row of activeData) {
152170
const id = row.instance_id;
171+
const ts = new Date(row.timestamp).getTime();
153172
const existing = uniqueInstances.get(id);
154173

155-
// Logic to pick the record with more metadata (prefer identified CPU)
156-
const hasCpu = row.cpu && row.cpu !== 'unknown';
157-
const existingHasCpu = existing && existing.cpu && existing.cpu !== 'unknown';
158-
159-
if (!existing || (!existingHasCpu && hasCpu)) {
174+
if (!existing || ts > new Date(existing.timestamp).getTime()) {
160175
uniqueInstances.set(id, row);
161176
}
162177
}
@@ -167,6 +182,11 @@ export default {
167182
const stats = {
168183
active_installs: activeCount,
169184
total_installs: Math.max(activeCount, totalCount),
185+
activity: activityData.map(row => ({
186+
date: row.day,
187+
pings: Number(row.pings) || 0,
188+
uniques: Number(row.uniques) || 0
189+
})),
170190
versions: [],
171191
countries: [],
172192
cpus: [],
@@ -568,7 +588,7 @@ export default {
568588
<main class="main">
569589
<div class="page-title">
570590
<h1>Usage Dashboard</h1>
571-
<p>Anonymous aggregate statistics from active VibeNVR installations · Last 7 days</p>
591+
<p>Anonymous aggregate statistics from active VibeNVR installations · Last 30 days</p>
572592
</div>
573593
574594
<!-- Error -->
@@ -591,7 +611,7 @@ export default {
591611
<div class="kpi-card">
592612
<div class="kpi-label"><svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 12h-4l-3 9L9 3l-3 9H2"/></svg> Active Installs</div>
593613
<div class="kpi-value" id="kpi-active">-</div>
594-
<div class="kpi-sub">Last 7 days</div>
614+
<div class="kpi-sub">Last 30 days</div>
595615
</div>
596616
<div class="kpi-card">
597617
<div class="kpi-label"><svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m7.5 4.27 9 5.15"/><path d="M21 8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16Z"/><path d="m3.3 7 8.7 5 8.7-5"/><path d="M12 22V12"/></svg> Total Installs</div>
@@ -633,6 +653,14 @@ export default {
633653
</div>
634654
</div>
635655
656+
<!-- Row 0b: Activity Trend -->
657+
<div class="chart-row cols-1">
658+
<div class="card">
659+
<div class="chart-title"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 12h-4l-3 9L9 3l-3 9H2"/></svg> Activity Trend (Last 30 Days)</div>
660+
<div class="chart-wrap" style="height:300px"><canvas id="chart-activity"></canvas></div>
661+
</div>
662+
</div>
663+
636664
<!-- Row 1: Cameras + Groups distribution -->
637665
<div class="chart-row cols-2">
638666
<div class="card">
@@ -848,6 +876,57 @@ export default {
848876
});
849877
mkChart('chart-os', 'doughnut', prepData(lastData.os), pp);
850878
mkChart('chart-arch', 'doughnut', prepData(lastData.arch), pp);
879+
880+
// Activity Trend Chart
881+
const activityCtx = document.getElementById('chart-activity')?.getContext('2d');
882+
if (activityCtx) {
883+
if (charts['chart-activity']) charts['chart-activity'].destroy();
884+
const activityLabels = lastData.activity.map(d => {
885+
const date = new Date(d.date);
886+
return date.toLocaleDateString(undefined, { month: 'short', day: 'numeric' });
887+
});
888+
charts['chart-activity'] = new Chart(activityCtx, {
889+
type: 'line',
890+
data: {
891+
labels: activityLabels,
892+
datasets: [
893+
{
894+
label: 'Unique IDs',
895+
data: lastData.activity.map(d => d.uniques),
896+
borderColor: tok('primary'),
897+
backgroundColor: 'transparent',
898+
tension: 0.3,
899+
pointRadius: 4,
900+
borderWidth: 3
901+
},
902+
{
903+
label: 'Total Pings',
904+
data: lastData.activity.map(d => d.pings),
905+
borderColor: tok('accent'),
906+
backgroundColor: 'transparent',
907+
tension: 0.3,
908+
pointRadius: 0,
909+
borderWidth: 2,
910+
borderDash: [5, 5]
911+
}
912+
]
913+
},
914+
options: {
915+
responsive: true, maintainAspectRatio: false,
916+
plugins: {
917+
legend: { position: 'top', labels: { color: tok('text') } },
918+
tooltip: {
919+
backgroundColor: tok('bg'), titleColor: tok('text'), bodyColor: tok('muted'),
920+
borderColor: tok('border'), borderWidth: 1, padding: 10, cornerRadius: 8
921+
}
922+
},
923+
scales: {
924+
x: { grid: { display: false }, ticks: { color: tok('muted'), maxRotation: 0 } },
925+
y: { grid: { color: tok('border') }, ticks: { color: tok('muted') }, beginAtZero: true }
926+
}
927+
}
928+
});
929+
}
851930
const distRaw = lastData.cameras_dist || [];
852931
mkChart('chart-cameras-dist', 'bar', { labels: distRaw.map(x=>x.name+' cam'), data: distRaw.map(x=>x.count) }, BAR_PALETTE());
853932
const gdistRaw = lastData.groups_dist || [];

0 commit comments

Comments
 (0)