Repository navigation
Conversation
Two aggregation-cycle cases seed raw reports whose numeric measures are missing from the first sample of a key (plain and byThread), or present on only some threads. Both fail on main: the fold stores NaN, and the byThread combination drops measures its first sample lacked. Dispatch-Task: harper-analytics-sparse-measure-nan Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KfL5oZzJbTmj6ZjiVWM8dc
aggregation() folded each later sample's numeric measures into the action created from a key's first sample. A measure that sample lacked had no accumulated value, so both Math.max(undefined, v) and the weighted mean stored NaN; and the mean was weighted by the action's count, so samples lacking a measure still weighed into it. Each fold target (an action, or one thread's record) now keeps cycle-local per-measure sample counts. A measure's first sample seeds it, peaks fold with max, means are weighted by the measure's own count. A measure present in every sample has a count equal to the action's, so its arithmetic is unchanged. The byThread combination summed only the measures of the key's first sample. It now accumulates every thread's numeric measures into the entry, so a measure absent from that sample or carried by only some threads is kept. Dispatch-Task: harper-analytics-sparse-measure-nan Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KfL5oZzJbTmj6ZjiVWM8dc
Dispatch-Task: harper-analytics-sparse-measure-nan Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KfL5oZzJbTmj6ZjiVWM8dc
…oss-thread sum A thread record's count, total and ratio are kept by its own fold and are not measures. Summing every numeric field of it made the aggregate total and ratio NaN for a byThread metric whose thread folded a sample carrying total. Dispatch-Task: harper-analytics-sparse-measure-nan Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KfL5oZzJbTmj6ZjiVWM8dc
… filtering them by name A thread's record is built from a sample's measures alone, so it never holds a total; folding total into it produced total and ratio NaN, which the previous commit filtered out of the cross-thread sum by name. That also discarded a caller measure named ratio. The fold now keeps total/ratio on non-thread actions only, and the cross-thread sum takes every numeric field again. Dispatch-Task: harper-analytics-sparse-measure-nan Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KfL5oZzJbTmj6ZjiVWM8dc
The thread-target check read the action's threads field, which the first sample's own fields are spread into. A non-byThread metric that reports a field named threads (null, 0, or a thread count such as 4) then had its second sample indexed into that value and the cycle threw before advancing the raw cursor, so every later cycle threw on the same record. The check now reads byThread, the flag that creates the per-thread records. Dispatch-Task: harper-analytics-sparse-measure-nan Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KfL5oZzJbTmj6ZjiVWM8dc
Release cherry-pick
|
There was a problem hiding this comment.
Code Review
This pull request updates the analytics aggregation logic to fold each measure over only the samples that carry it, tracking per-measure sample counts in a cycle-local map. It also updates the design documentation and adds comprehensive unit tests. The review feedback correctly identifies two critical issues: first, the period property is not destructured from entry in the thread-averaging loop, which incorrectly resets the aggregation period to 0 for thread-based metrics; second, a missing total property on the first sample can lead to NaN propagation. Both issues are accompanied by actionable code suggestions to resolve them, along with a recommendation to add a corresponding test assertion.
Analytics aggregation now folds each numeric measure over only the samples that carry it, so a measure missing from a metric's first sample no longer stores
NaN(or is dropped, for per-thread metrics), and measures present in every sample aggregate exactly as before.⊙ Problem
Analytics aggregation stores
NaNfor a numeric measure that the first raw sample of a metric key did not carry.aggregation()builds each aggregate action from the key's first sample, then folds later samples into it; for a measure missing from that first sample bothMath.max(undefined, v)(peak-namedmax*measures, added in fix(analytics): report aggregate maxDepth as the sum of per-thread peaks) and the count-weighted mean evaluate toNaN, and theNaNis written tohdb_analytics. ForbyThreadmetrics, the cross-thread combination summed only the measures of the key's very first sample, so a measure that sample lacked, or that only some threads carry, was dropped from the stored row. Sparse measures come from callback metrics (recordAction(fn)), analytics listeners and raw backlog from other versions.A second, older defect sits in the same fold: the mean was weighted by the action's total sample count, so samples that lacked a measure still weighed into its mean (values 6 and 12, each count 2, with two samples lacking it between them, averaged to 8 instead of 9).
💡 Solution
byThreadmetrics) keeps the count of the samples that carried each measure in a cycle-localmeasureCountsmap, never on the action thatstoreMetricpersists and listeners receive. A measure's first sample seeds it,max[A-Z]measures fold withMath.max, and other measures are the running mean weighted by their own count.total/ratioon thread records. A thread's record is built from a sample's measures alone and never holds atotal, so foldingtotalinto it producedtotal/ratioNaN; the fold now keepstotal/ratioon non-thread actions only. With that, everything on a thread record is a measure (plus itscount, which the thread count replaces), and a caller measure namedratiois summed like any other.byThread. Whether a sample folds into a thread record is now decided by the action'sbyThreadflag (which creates the thread records), not by itsthreadsfield, which the first sample's own fields are spread into. Onmain, a non-byThreadmetric reporting its ownthreadsfield (for example a thread count,threads: 4) throwsTypeError: Cannot create property '1' on number '4'on its second sample before the raw cursor advances, so every later cycle throws on the same record and aggregation stops.resources/analytics/DESIGN.mdand indexed from the rootDESIGN.md.⚖️ Alternatives
Framing-Verdict: better-alternative-exists; the alternative was adopted: accumulate the cross-thread sum directly into the entry instead of first building the union of thread measure names and summing per name. The union design as first written still gated each name on the entry's original field, so it dropped every newly discovered measure; the direct pass visits only present fields.✅ Verification
runAggregationCycleover realhdb_raw_analytics/hdb_analyticstables and read the stored rows. The live sparse producers are callback/listener metrics; the seeded raw reports stand in for them.unitTests/resources/analytics/aggregationCycle.test.js— three new cases, on shared seed/drain/wait/assertNoNaNhelpers:maxLatencyandlateabsent from the first sample,rareabsent from the middle ones, a zero-valued firstlate, an unequal first count; asserts noNaN, per-measure weighted means, and the densemeanat its exact running-mean value0.28571428571428575.byThread: thread 0's first sample carries onlydepth;maxDepth/queuedappear later;stalledonly on thread 7; thread 7 also carriestotaland aratiomeasure; asserts per-thread peaks and means summed,ratio0.5, and noNaN.threadsfield: a non-byThreadmetric whose samples carrythreads: 4folds to mean 3, count 2 (throwsTypeErroronmain).origin/main(a4e217a) dist, the mean/peak case fails onmaxLatency=NaNand thebyThreadcase onmaxDepthundefined !== 14; thetotalandratiofixture fields each fail at the commit before their fix (total=NaN;ratioundefined !== 0.5). All 7 pass with the change, on RocksDB andHARPER_STORAGE_ENGINE=lmdb.test:unit:resources4135 passing / 0 failing;test:unit:main6841 passing / 0 failing;test:integration:all2338 pass / 0 fail / 6 cancelled (all six in the Ollama backend suite, which needs a local Ollama with default models; unrelated). Prettier,oxlintandcheck:design-docsclean.Refs #3017
🤖 Generated by Claude Opus 5.5 (Claude Code dev-agent); posted via @kriszyp.
🤖 Generated with Claude Code
https://claude.ai/code/session_01KfL5oZzJbTmj6ZjiVWM8dc
Related PRs: #3110 overlaps (rewrites the same aggregation fold and byThread combination; whichever lands second resolves the conflict)
Complexity: medium
Dispatch: task
harper-analytics-sparse-measure-nan· queued by unknown · ran by claude/opus/xhigh · worker kzyp-xps-1Review-Coverage: authored=claude; ran=cursor-muse,codex,gemini; adjudicated=domain; declined=cursor-grok,cursor-composer,cursor-kimi; rounds=5; full=2 @ c81fb79
Review-Attention: study ~15m (critical: write.ts; decisions: weight-not-persisted, absent-means-unreported, thread-sum-union, sparse-total-scope, ratio-denominator, absent-measure-semantics, partial-thread-sum, running-mean-kept, no-backfill) @ c81fb79