Workflow health: bucketed run counts behind the window - #5162
Open
midigofrank wants to merge 3 commits into
Open
Workflow health: bucketed run counts behind the window#5162midigofrank wants to merge 3 commits into
midigofrank wants to merge 3 commits into
Conversation
The outcomes and failures slices both aggregate, so neither can answer when the traffic arrived. Add Stats.runs/2 and GET /health/runs, which return every run that reached a final state in the window, oldest first, so a chart can bucket them over time. Runs are filtered on inserted_at rather than the state transition, so a run stays in the bar its attempt started in. The redundant last_activity condition on the work order is there for the planner: it lets the existing index narrow the work orders before the nested loop into runs.
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## frank/con-106 #5162 +/- ##
===============================================
- Coverage 90.8% 90.8% -0.0%
===============================================
Files 422 422
Lines 20864 20881 +17
===============================================
+ Hits 18950 18959 +9
- Misses 1914 1922 +8 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
Stats.runs/2 returned every final run in the window and left the browser to bucket them. On a busy workflow that's six figures of rows, cached whole and JSON-encoded, to draw thirty bars. Count them in Postgres instead: 2-hourly over a day, AM/PM over a week, daily over a month. The grid is aligned to the clock, so integer division by the bucket width is the whole of the bucketing — no date_trunc case per width — and a bar labelled "2am" or "Tuesday" is telling the truth. extract(epoch from ...) yields numeric and the cast to bigint rounds, so the query floors first; without it a run at 01:59:59.7 counts in the 02:00 bar. Each window carries one bucket more than its width divides into. `now` sits mid-bucket, so a grid of exactly days_back / width bars would start after now - days_back and leave the oldest hours of the window undrawn while the donuts beside it counted them. The oldest bar reaches back past `from` instead, and `window` reports the range actually covered. Buckets come back zero-filled across every final state and flat rather than nested, which is the row shape Recharts takes as `data`.
midigofrank
marked this pull request as ready for review
September 9, 2026 17:23
Security Review ✅
|
con-106 replaced the health page's push-based cache invalidation with a change marker in the cache key; con-182 added the bucketed runs chart beside it. Kept the runs slice, dropped `Stats.invalidate/1` and its test.
lmac-1
force-pushed
the
frank/con-182
branch
from
September 9, 2026 18:30
e561399 to
77d6c89
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
The outcomes and failures panels both aggregate over the whole window, so neither can answer when the traffic arrived. Add
Stats.runs/2andGET /health/runs: final run counts per state, bucketed over time — 2-hourly over a day, AM/PM over a week, daily over a month.The bucketing is done in Postgres, not the browser. The alternative is shipping every run in the window — six figures of rows on a busy workflow, cached whole and JSON-encoded — to draw thirty bars.
Runs are counted on
inserted_atrather than the state transition, so a run stays in the bar its attempt started in. The redundantlast_activitycondition on the work order is there for the planner: it lets the existingwork_orders(workflow_id, last_activity)index narrow the work orders before the nested loop intoruns(work_order_id, inserted_at).Response
{ "window": { "from": "2026-09-08T12:00:00Z", "to": "2026-09-09T13:47:09.512345Z" }, "buckets": [ { "at": "2026-09-08T12:00:00Z", "success": 41, "failed": 2, "crashed": 0, "cancelled": 0, "killed": 0, "exception": 0, "lost": 0 }, { "at": "2026-09-08T14:00:00Z", "success": 38, "failed": 0, "crashed": 1, "cancelled": 0, "killed": 0, "exception": 0, "lost": 0 }, "… 11 more, through 2026-09-09T12:00:00Z" ] }Three things about that shape, all of them for the chart:
A bucket is flat, not nested.
{ at, success, failed, … }is the row shape Recharts takes asdata, so the list goes to the chart untouched and eachBarnames the state it draws:No
reduceto pivot counts into rows, nogroupByin the client, and a state added toRun.final_states/0shows up as a key rather than as a silently dropped row.Every bucket carries every state, zero-filled, so the chart draws a flat window without reasoning about which bars are missing — an idle day is 13 bars of zero, not an empty box. (This is the case the donut needed an explicit
emptyMessagebranch for; a bar chart doesn't.)Boundaries sit on the clock, so
atcan be labelled honestly and the labels don't shift with whatever minute the request landed on:Every bucket width divides a day evenly, and
window.fromis aligned to the width, so integer division by the width is the whole of the bucketing in SQL — nodate_truncspecial case per range.Two details worth a look in review
extract(epoch from …)yieldsnumeric, andnumeric::bigintrounds. Without thefloora run at 01:59:59.7 is counted in the 02:00 bar; there's a test pinned a hair under a boundary for exactly this.Each window carries one bucket more than its width divides into (13 / 15 / 31, not 12 / 14 / 30).
nowsits mid-bucket, so a grid of exactlydays_back / widthbars would start afternow - days_backand leave the oldest hours of the window undrawn while the donuts beside it counted them. The oldest bar reaches back pastfrominstead, andwindowreports the range the bars actually cover — the last bar is the onenowfalls in, so it's still filling.Closes CON-182
AI Usage
Please disclose whether you've used AI anywhere in this PR (it's cool, we just
want to know!):
You can read more details in our
Responsible AI Policy
Pre-submission checklist
/reviewwith Claude Code)
(e.g.,
:owner,:admin,:editor,:viewer)