fix(analytics): avg duration handles both quoted and unquoted duration_s#1728
fix(analytics): avg duration handles both quoted and unquoted duration_s#1728RyanAlberts wants to merge 1 commit into
Conversation
The per-skill (avg X) column in gstack-analytics silently rendered as "0s" for any /skill whose completion events emit "duration_s" as a quoted JSON string. The completion-status preamble that every skill runs (scripts/resolvers/preamble/generate-completion-status.ts:70) and gstack-codex-probe both emit "duration_s":"<N>", so the dominant case was wrong; gstack-telemetry-log emits the unquoted form which is why the bug didn't show up everywhere. The AVG_DUR awk block split each line on the literal "duration_s": prefix and then ran gsub(/[^0-9.].*/, "", parts[2]). For unquoted 600,"ts":... the gsub matches at the comma and yields "600". For quoted "600","ts":... it matches at the leading double-quote (position 0) and the trailing .* consumes the entire rest of the line, leaving an empty string that never accumulates. The companion TOTAL_DURATION calculation (line 114) uses a different parser keyed on -F'[:,]' field splitting + non-greedy gsub, so it isolates the value before stripping and was always correct. Only the per-skill average was broken. Fix: strip an optional leading double-quote with sub(/^"/, ...) before the existing gsub so quoted and unquoted forms take the same path. Adds test/gstack-analytics-avg-duration.test.ts covering quoted-only, unquoted-only, mixed, and total-time aggregation. The test fails on the pre-fix tree (expected "2m", got "0s") and passes after the fix.
|
Independent validation — this looks correct. Confirmed the bug on Running this PR's No competing open PR touches |
Summary
gstack-analytics(the local-usage dashboard) silently displayed(avg 0s)next to any/skillwhose completion events emit"duration_s"as a quoted JSON string. The completion-status preamble that every skill runs (scripts/resolvers/preamble/generate-completion-status.ts:70) andbin/gstack-codex-probe:84both emit"duration_s":"<N>", so the dominant in-the-wild case rendered as0sand most per-skill averages in the dashboard have been wrong since this code path was added.bin/gstack-telemetry-logemits the unquoted numeric form, which is why the bug wasn't visible for every skill.The companion
Total time:line at the bottom of the dashboard was always correct — it uses a different parser, so the bug was confined to the per-/skill(avg X)column.Current behavior on upstream main
Repro on
origin/main(cf50443b):Before:
Total time: 18mmatches the real sum (120 + 180 + 60 + 300 + 420 = 1080s = 18m), confirming the total-time parser is already correct. Only the per-skill(avg ...)column is broken.Root cause
The
AVG_DURblock inbin/gstack-analytics:165-177splits each line on the literal"duration_s":prefix and then strips non-numeric characters fromparts[2]:600,"ts":...→gsubmatches at the comma →parts[2]becomes600→ accumulates."600","ts":...→gsubmatches at the leading"(position 0), and the trailing.*consumes the rest of the line →parts[2]becomes the empty string →"" + 0 > 0is false → never accumulates →countstays0→ average renders as0.TOTAL_DURATION(bin/gstack-analytics:114-125) uses-F'[:,]'field splitting and a non-greedygsub(/[^0-9.]/, "", val), which isolates the value before stripping. That parser handles both forms and isn't affected.Fix
Strip an optional leading double-quote with
sub(/^"/, "", parts[2])before the existinggsubso quoted and unquoted forms take the same path through the existing digit-extraction:Three lines in
bin/gstack-analytics(one new statement + two-line comment update). No producer-side changes — the parser fix accommodates both emitter formats that are already in the wild, so existing JSONL files on users' disks render correctly without migration.Edge cases verified by hand:
sub(/^"/)gsub(/[^0-9.].*/)600,"ts":...(unquoted)600,"ts":...600"600","ts":...(quoted)600","ts":...600null,"ts":...null,"ts":..."" + 0 > 0is false"1.5","ts":...(decimal)1.5","ts":...1.5Validation
After the fix, same repro renders correctly:
New regression test in
test/gstack-analytics-avg-duration.test.tsexercises four cases (quoted-only, unquoted-only, mixed quoted+unquoted for the same skill, and total-time aggregation). I confirmed it fails on the pre-fix tree (expected "2m", got "0s") by stashing the patch and re-running, then passes after popping the stash.Scope
bin/gstack-analytics(3 lines) + 1 new test file. No SKILL.md/template changes, no VERSION/CHANGELOG bumps (per the wave-process note in CONTRIBUTING.md). The producer-side emitters (generate-completion-status.ts,gstack-codex-probe,gstack-telemetry-log) are untouched — fixing the parser keeps existing JSONL on users' machines readable without a migration.