Skip to content

Commit 6b0cabc

Browse files
authored
Merge pull request #996 from SonicJs-Org/lane711/stats-parse-display-fix
fix(stats): fix dashboard data parsing and display issues
2 parents 036baf3 + 7319b63 commit 6b0cabc

4 files changed

Lines changed: 135 additions & 18 deletions

File tree

packages/core/src/app.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -383,7 +383,7 @@ export function createSonicJSApp(config: SonicJSConfig = {}): SonicJSApp {
383383
app.use('*', metricsMiddleware())
384384

385385
// Bootstrap middleware - runs migrations, syncs collections, and initializes plugins
386-
app.use('*', bootstrapMiddleware(config))
386+
app.use('*', bootstrapMiddleware(config, [...corePluginsBeforeCatchAll, ...corePluginsAfterCatchAll, ...(config.plugins?.register ?? [])]))
387387

388388
// bootIsolate — extracted from the wiring middleware so it can be called from
389389
// both HTTP requests AND cron-first cold isolates (scheduled() handlers) that

packages/core/src/middleware/bootstrap.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ export function verifySecurityConfig(env: Bindings): void {
8282
* Bootstrap middleware that ensures system initialization
8383
* Runs once per worker instance
8484
*/
85-
export function bootstrapMiddleware(config: SonicJSConfig = {}) {
85+
export function bootstrapMiddleware(config: SonicJSConfig = {}, allPlugins?: Array<{ name?: string; id?: string }>) {
8686
return async (c: Context<{ Bindings: Bindings; Variables: { hookSystem?: unknown } }>, next: Next) => {
8787
// Attach the hook system to the request BEFORE any heavy bootstrap work
8888
// runs, so anything that emits a hook during bootstrap (cron cold starts,
@@ -233,8 +233,9 @@ export function bootstrapMiddleware(config: SonicJSConfig = {}) {
233233
}
234234
}
235235

236-
// Plugin names from config
237-
const activePlugins = (config.plugins?.register ?? []).map((p: any) => p.name ?? 'unknown');
236+
// Plugin names — use allPlugins (all registered, including core) when available
237+
const pluginSource = allPlugins ?? (config.plugins?.register ?? []) as Array<{ name?: string; id?: string }>;
238+
const activePlugins = pluginSource.map((p) => p.name ?? String(p.id ?? 'unknown'));
238239

239240
// Stable installation ID via KV (generated once, persisted)
240241
let installationId = 'unknown';

packages/stats/src/plugins/stats-dashboard/routes/admin.ts

Lines changed: 46 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -129,13 +129,14 @@ adminRoutes.get('/', async (c) => {
129129
FROM documents WHERE ${EVENTS_WHERE} AND json_extract(data,'$.event_type')='installation_started'
130130
GROUP BY t ORDER BY count DESC`
131131
).all(),
132-
// 9. Install failures by errorType
132+
// 9. Install failures by errorType + step + version
133133
db.prepare(
134134
`SELECT COALESCE(json_extract(data,'$.properties.errorType'), json_extract(data,'$.error_code'),'unknown') AS err,
135-
COALESCE(json_extract(data,'$.step'),'-') AS step,
135+
COALESCE(json_extract(data,'$.properties.step'),'-') AS step,
136+
COALESCE(json_extract(data,'$.properties.version'),'-') AS version,
136137
COUNT(*) AS count
137138
FROM documents WHERE ${EVENTS_WHERE} AND json_extract(data,'$.event_type')='installation_failed'
138-
GROUP BY err, step ORDER BY count DESC LIMIT 25`
139+
GROUP BY err, step, version ORDER BY count DESC LIMIT 25`
139140
).all(),
140141
// 10. Runtime errors (error_occurred) by errorType + version
141142
db.prepare(
@@ -145,18 +146,34 @@ adminRoutes.get('/', async (c) => {
145146
FROM documents WHERE ${EVENTS_WHERE} AND json_extract(data,'$.event_type')='error_occurred'
146147
GROUP BY err, version ORDER BY count DESC LIMIT 25`
147148
).all(),
148-
// 11. Top collections from project_snapshot (aggregate doc counts across installations)
149+
// 11. Top collections from project_snapshot — latest snapshot per installation to avoid boot-count inflation
149150
db.prepare(
150-
`SELECT key AS collection, SUM(CAST(value AS INTEGER)) AS total_docs, COUNT(DISTINCT json_extract(data,'$.properties.installation_id')) AS installations
151-
FROM documents, json_each(json(json_extract(data,'$.properties.collection_counts')))
152-
WHERE ${EVENTS_WHERE} AND json_extract(data,'$.event_type')='project_snapshot'
151+
`WITH latest AS (
152+
SELECT json_extract(data,'$.properties.installation_id') AS installation_id,
153+
json_extract(data,'$.properties.collection_counts') AS counts_json
154+
FROM documents
155+
WHERE ${EVENTS_WHERE} AND json_extract(data,'$.event_type')='project_snapshot'
156+
GROUP BY json_extract(data,'$.properties.installation_id')
157+
HAVING created_at = MAX(created_at)
158+
)
159+
SELECT key AS collection,
160+
SUM(CAST(value AS INTEGER)) AS total_docs,
161+
COUNT(DISTINCT installation_id) AS installations
162+
FROM latest, json_each(json(counts_json))
153163
GROUP BY key ORDER BY total_docs DESC LIMIT 20`
154164
).all(),
155-
// 12. Top plugins from project_snapshot (count appearances across installations)
165+
// 12. Top plugins from project_snapshot — latest snapshot per installation
156166
db.prepare(
157-
`SELECT value AS plugin, COUNT(DISTINCT json_extract(data,'$.properties.installation_id')) AS installations
158-
FROM documents, json_each(json(json_extract(data,'$.properties.active_plugins')))
159-
WHERE ${EVENTS_WHERE} AND json_extract(data,'$.event_type')='project_snapshot'
167+
`WITH latest AS (
168+
SELECT json_extract(data,'$.properties.installation_id') AS installation_id,
169+
json_extract(data,'$.properties.active_plugins') AS plugins_json
170+
FROM documents
171+
WHERE ${EVENTS_WHERE} AND json_extract(data,'$.event_type')='project_snapshot'
172+
GROUP BY json_extract(data,'$.properties.installation_id')
173+
HAVING created_at = MAX(created_at)
174+
)
175+
SELECT value AS plugin, COUNT(DISTINCT installation_id) AS installations
176+
FROM latest, json_each(json(plugins_json))
160177
GROUP BY value ORDER BY installations DESC LIMIT 20`
161178
).all(),
162179
// 13. Field type histogram aggregated across all snapshots
@@ -228,7 +245,7 @@ adminRoutes.get('/', async (c) => {
228245
const s = t.get('installation_started') ?? 0
229246
const comp = t.get('installation_completed') ?? 0
230247
const f = t.get('installation_failed') ?? 0
231-
return { week: w, started: s, completed: comp, failed: f, rate: s > 0 ? Math.round((comp / s) * 100) : 0 }
248+
return { week: w, started: s, completed: comp, failed: f, rate: s > 0 ? Math.round((comp / s) * 100) : 0, avgPerDay: (comp / 7).toFixed(1) }
232249
})
233250

234251
// ── Totals / KPIs ──────────────────────────────────────────────────────
@@ -265,7 +282,7 @@ adminRoutes.get('/', async (c) => {
265282
count: Number(r.count),
266283
}))
267284
const templateRows = rowsOf(templateR) as { t: string; count: number }[]
268-
const installFailRows = rowsOf(installFailR) as { err: string; step: string; count: number }[]
285+
const installFailRows = rowsOf(installFailR) as { err: string; step: string; version: string; count: number }[]
269286
const runtimeErrRows = rowsOf(runtimeErrR) as { err: string; version: string; count: number }[]
270287

271288
// ── Project snapshot breakdowns ─────────────────────────────────────────
@@ -378,6 +395,7 @@ adminRoutes.get('/', async (c) => {
378395
<th class="py-2 text-left font-medium">Week</th>
379396
<th class="py-2 text-right font-medium">Started</th>
380397
<th class="py-2 text-right font-medium">Completed</th>
398+
<th class="py-2 text-right font-medium">Avg/day</th>
381399
<th class="py-2 text-right font-medium">Failed</th>
382400
<th class="py-2 text-right font-medium">Completion %</th>
383401
</tr>
@@ -387,14 +405,28 @@ adminRoutes.get('/', async (c) => {
387405
<td class="py-2 font-mono text-zinc-700 dark:text-zinc-300">${fmtWeekDate(r.week)}</td>
388406
<td class="py-2 text-right text-zinc-700 dark:text-zinc-300">${r.started.toLocaleString()}</td>
389407
<td class="py-2 text-right text-emerald-600 dark:text-emerald-400 font-medium">${r.completed.toLocaleString()}</td>
408+
<td class="py-2 text-right text-zinc-500 dark:text-zinc-400">${r.avgPerDay}</td>
390409
<td class="py-2 text-right ${r.failed > 0 ? 'text-red-600 dark:text-red-400' : 'text-zinc-400'}">${r.failed.toLocaleString()}</td>
391410
<td class="py-2 text-right"><span class="inline-flex rounded-full px-2 py-0.5 text-xs font-medium ${r.rate >= 70 ? 'bg-emerald-50 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-400' : r.rate >= 40 ? 'bg-amber-50 text-amber-700 dark:bg-amber-900/30 dark:text-amber-400' : 'bg-red-50 text-red-700 dark:bg-red-900/30 dark:text-red-400'}">${r.rate}%</span></td>
392411
</tr>`).join('')}
393412
</tbody></table></div>`)}
394413
395414
<!-- Errors -->
396415
<div class="grid grid-cols-1 gap-6 lg:grid-cols-2">
397-
${card('Install Failures', 'installation_failed grouped by error + step', errTable(installFailRows.map((r) => ({ err: r.err, sub: r.step, count: Number(r.count) })), 'Step', 'No failures recorded.'))}
416+
${card('Install Failures', 'installation_failed grouped by error + step + version', installFailRows.length === 0
417+
? '<div class="py-8 text-center text-sm text-zinc-500 dark:text-zinc-400">No failures recorded.</div>'
418+
: `<div class="overflow-x-auto"><table class="w-full text-sm">
419+
<thead class="text-xs uppercase tracking-wide text-zinc-500 dark:text-zinc-400">
420+
<tr><th class="py-2 text-left font-medium">Error</th><th class="py-2 text-left font-medium">Step</th><th class="py-2 text-left font-medium">Version</th><th class="py-2 text-right font-medium">Count</th></tr>
421+
</thead>
422+
<tbody class="divide-y divide-zinc-950/5 dark:divide-white/5">
423+
${installFailRows.map((r) => `<tr>
424+
<td class="py-2 pr-4 text-zinc-700 dark:text-zinc-300 max-w-xs break-words">${esc(r.err)}</td>
425+
<td class="py-2 pr-4 font-mono text-xs text-zinc-500 dark:text-zinc-400">${esc(r.step)}</td>
426+
<td class="py-2 pr-4 font-mono text-xs text-zinc-500 dark:text-zinc-400">${esc(r.version)}</td>
427+
<td class="py-2 text-right font-semibold text-red-600 dark:text-red-400">${Number(r.count).toLocaleString()}</td>
428+
</tr>`).join('')}
429+
</tbody></table></div>`)}
398430
${card('Runtime Errors', 'error_occurred grouped by error + version', errTable(runtimeErrRows.map((r) => ({ err: r.err, sub: r.version, count: Number(r.count) })), 'Version', 'No runtime errors recorded.'))}
399431
</div>
400432
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import { test, expect } from '@playwright/test'
2+
import { loginAsAdmin } from './utils/test-helpers'
3+
4+
test.describe('Stats dashboard data parsing', () => {
5+
test.beforeEach(async ({ page }) => {
6+
await loginAsAdmin(page)
7+
})
8+
9+
test('dashboard renders without JS errors', async ({ page }) => {
10+
const errors: string[] = []
11+
page.on('pageerror', (err) => errors.push(err.message))
12+
13+
await page.goto('/admin/dashboard')
14+
await page.waitForLoadState('networkidle')
15+
16+
expect(errors.filter((e) => !e.includes('Chart'))).toHaveLength(0)
17+
})
18+
19+
test('install failures table has Step and Version columns', async ({ page }) => {
20+
await page.goto('/admin/dashboard')
21+
await page.waitForLoadState('networkidle')
22+
23+
const failuresCard = page.locator('text=Install Failures').first()
24+
await expect(failuresCard).toBeVisible()
25+
26+
const headers = page.locator('th')
27+
const headerTexts = await headers.allTextContents()
28+
const normalized = headerTexts.map((t) => t.toLowerCase())
29+
30+
// Table should have Step and Version columns (not just Error + Count)
31+
expect(normalized.some((h) => h.includes('step'))).toBe(true)
32+
expect(normalized.some((h) => h.includes('version'))).toBe(true)
33+
})
34+
35+
test('What People Build section renders when snapshot data exists', async ({ page }) => {
36+
await page.goto('/admin/dashboard')
37+
await page.waitForLoadState('networkidle')
38+
39+
await expect(page.locator('text=What People Build')).toBeVisible()
40+
// Section header is always present; charts render if data exists
41+
await expect(page.locator('text=Top Collections')).toBeVisible()
42+
await expect(page.locator('text=Active Plugins')).toBeVisible()
43+
})
44+
45+
test('/v1/events accepts project_snapshot payload', async ({ request }) => {
46+
const res = await request.post('/v1/events', {
47+
data: {
48+
data: {
49+
installation_id: 'test-e2e-install',
50+
event_type: 'project_snapshot',
51+
properties: {
52+
installation_id: 'test-e2e-install',
53+
collection_names: '["test_col"]',
54+
collection_counts: '{"test_col":5}',
55+
active_plugins: '["Test Plugin"]',
56+
field_type_histogram: '{"string":3}',
57+
doc_total: 5,
58+
sonicjs_version: '3.0.0-test',
59+
},
60+
},
61+
},
62+
})
63+
expect(res.status()).toBe(201)
64+
const body = await res.json()
65+
expect(body.success).toBe(true)
66+
})
67+
68+
test('/v1/events accepts installation_failed with step', async ({ request }) => {
69+
const res = await request.post('/v1/events', {
70+
data: {
71+
data: {
72+
installation_id: 'test-e2e-fail',
73+
event_type: 'installation_failed',
74+
properties: {
75+
errorType: 'Command failed with exit code 1',
76+
step: 'db_migrate',
77+
version: '3.0.0-test',
78+
},
79+
},
80+
},
81+
})
82+
expect(res.status()).toBe(201)
83+
})
84+
})

0 commit comments

Comments
 (0)