OXYGENHUB
◆ v0.9.20 oxygenhub / docs / reports.md
11.7 KiB · Markdown · last changed Flow job reports and timings over the API, oh run report and oh report; 0.9.14 (closes #348) 7 days ago renderedsource blame history raw
50d93fa4 Andy McCall ↶ 1
# Reports
2
3
*Design, 2026-10-11. Supersedes the separate Coverage and Tests tabs
4
(spec §13.6, §13.7), which become two kinds of report.*
5
6
A project produces more than pass and fail: coverage, test results,
7
frame timings, sizes, benchmark curves. Today the hub reads two of them,
8
LCOV coverage and JUnit tests, each with a tab of its own. This design
9
makes reports one thing: a project declares what a report is, the hub
10
keeps every run's copy, and readers move through its history.
11
12
## 1. What a report is
13
14
A **report** is a directory a Flow job writes, or anyone uploads:
15
16
```
17
report/
18
  report.toml        the declaration (required)
19
  index.html         a page to show (custom and performance kinds)
20
  data/*.csv|json    datasets the page draws from
21
  assets/…           images, scripts, styles: everything the page needs
22
```
23
24
`report.toml` says what it is:
25
26
```toml
27
kind    = "performance"        # coverage | tests | performance | custom
28
title   = "Frame timing"
29
summary = "Median frame 16.4 ms, p99 18.1 ms over 12 scenes"
30
31
[metrics]                      # the numbers the hub tracks across runs
32
median_frame_ms = { value = 16.4, unit = "ms", better = "lower" }
33
p99_frame_ms    = { value = 18.1, unit = "ms", better = "lower", fail_above = 20 }
34
scenes          = { value = 12 }
260c33b4 Andy McCall ↶ 35
36
[context]                      # free-form, shown with the report
37
target_fps    = 60
38
budget_cycles = 133333
50d93fa4 Andy McCall ↶ 39
```
40
41
- **Built-in kinds** name their data file and need no page:
17dc8de4 Andy McCall ↶ 42
  `kind = "coverage"` with `file = "lcov.info"` (LCOV, Cobertura or JaCoCo), and
50d93fa4 Andy McCall ↶ 43
  `kind = "tests"` with `file = "junit.xml"`. The hub reads them as it
44
  does today, and works out their metrics itself (line coverage; passed,
45
  failed, skipped).
46
- **`performance`** is a page with metrics, as above.
47
- **`custom`** is a page, metrics optional.
260c33b4 Andy McCall ↶ 48
- **`[context]`** is free-form: what a reader needs to read the numbers,
49
  shown with the report and kept per run, such as a performance budget
50
  (`target_fps = 30`, `budget_cycles = 268800`). The hub assumes no frame
51
  rate or budget; each report's metrics and thresholds carry their own.
50d93fa4 Andy McCall ↶ 52
- **A metric** is a number with an optional `unit`, `better` (`higher` or
53
  `lower`, for colouring changes) and `fail_above` / `fail_below`, which
54
  set a check on the change (§5).
55
56
**Implicit declarations:** a job artifact named `coverage` or `junit`, the
57
names used today, is read as a report of that kind with no `report.toml`,
58
so every existing pipeline keeps working unchanged.
59
60
## 2. How a report arrives
61
62
**From Flow**, a job declares it in its outputs:
63
64
```kdl
65
job "perf" {
66
    exec "sh" "-eu" ".flow/jobs/perf.sh"
67
    output {
68
        report "frame-timing" {
69
            path "/src/out/report"
70
            when "always"
71
        }
72
    }
73
}
74
```
75
76
The runner uploads the directory as one bundle. `when "always"` keeps a
77
failing job's report, as for artifacts.
78
a36bab6b Andy McCall ↶ 79
The bundle travels file by file: each to
80
`POST /api/v1/flow/jobs/{job}/reports/{name}/files` with its path in
81
`X-Flow-Report-Path`, answered with its blob; then the manifest,
82
`{"version":1,"files":[{"path","blob","size"}]}`, as the job's artifact
83
under the report's name, which the receipt names. The hub refuses a
84
manifest naming a file it does not hold, reads `report.toml` and indexes
85
the report; a `report.toml` it cannot read is kept as a custom report whose
86
summary says why, so the job's log and the tab show what to fix. Paths are
87
`/`-separated names of letters, digits, `.`, `_`, `-` and `+`; a report
88
holds at most 2000 files and 512 MiB.
89
50d93fa4 Andy McCall ↶ 90
**From anywhere else:**
91
`oh report upload frame-timing ./report --bookmark develop` attaches it to
92
the bookmark's current state, or `--change ID` to a change's. The API is
93
`POST /repos/{o}/{n}/reports/{name}` with the bundle as a tar.
94
95
## 3. How the hub keeps it
96
97
Three layers.
98
99
1. **The bundle, per job, content-addressed.** Each file is a blob in the
100
   repository's own store, chunked and deduplicated as artifacts are, and
101
   the bundle is a small **tree**: a manifest of path → blob. A file that
102
   did not change since the last run (the page, its scripts, a dataset that
103
   did not move) is stored once however many runs name it; one that
104
   changed a little costs only its changed chunks. Keeping every run's
105
   report is cheap.
106
2. **An index in the database:**
107
   - `reports`: one row per report per job or upload: repository, name,
108
     kind, title, summary, run and job (or uploader), bookmark, the
109
     measured state and heads, time, and the tree's blob.
110
   - `report_metrics`: one row per metric per report: key, value, unit,
111
     better, thresholds.
a36bab6b Andy McCall ↶ 112
   - `report_files`: one row per file per report: path, blob, size; what
113
     garbage collection keeps and the frame serves.
50d93fa4 Andy McCall ↶ 114
115
   History, trends, the scrubber and checks read only this. Coverage's
116
   and Tests' existing tables become their kinds' details, keyed by
117
   report.
118
3. **Retention, by policy:**
119
   - metrics: forever (they are a few bytes a run);
120
   - bundles: every run on the default bookmark; the last 50 on any other
121
     bookmark; a proposal's until it lands or closes plus 30 days; a tagged
122
     release's forever. Set per repository in Settings › Reports.
123
124
   An expired bundle's row keeps its metrics and loses its tree; the blobs
125
   go at the next garbage collection.
126
127
**Garbage collection** keeps every tree and file blob a kept report names,
128
as it keeps artifacts (oxygenhub#126): the database is the only thing that
129
names them.
130
131
## 4. What readers see
132
133
- **A Reports tab** (in place of Coverage and Tests), shown once a
134
  repository has a report: each report's latest on the chosen bookmark,
135
  with its kind, summary, metrics and their change against the run
136
  before. Coverage and Tests are entries in it; their old addresses
137
  redirect.
138
- **A report's page:**
139
  - **built-in kinds** show their own views (the coverage tree and file
140
    view, the test cases and their history), as today;
141
  - **performance and custom** show `index.html` in a **sandboxed frame
142
    served from a separate origin**, with no network access and no access
143
    to the hub's cookies or session, so a page may run any scripts and
144
    charts it likes and still cannot act as the viewer;
145
  - **metrics** as a strip of numbers with sparklines across runs.
146
- **The timeline:** along the top, one stop per run on the bookmark,
147
  oldest to newest, each labelled with the change it measured.
148
  - **Scrub** to any run, or step with the arrow keys.
149
  - **Play** steps through the runs at a chosen speed, so a chart can be
150
    watched changing.
151
  - **Compare** two runs side by side, with every metric's delta; on a
152
    proposal the default is its base against its head.
153
- **A frame interface** for pages that animate rather than reload: inside
154
  the frame, `report.run()` gives the run on screen, and
155
  `report.history("dataset")` that dataset across the runs, so a page can
156
  redraw as the timeline moves or chart its own trends.
157
- **Downloads:** the whole bundle as a zip, and each dataset alone.
158
159
## 5. Checks
160
161
A metric with `fail_above` or `fail_below` sets a check named
162
`report/NAME/metric` on the change, failing when crossed, which a proposal
163
may require (Settings › Protection). Coverage's existing check becomes
164
this.
165
166
## 6. The API and `oh`
167
fac6853c Andy McCall ↶ 168
Built (oxygenhub#348), read access as the repository's pages:
50d93fa4 Andy McCall ↶ 169
fac6853c Andy McCall ↶ 170
- `GET /repos/{o}/{n}/flow/runs/{n}/jobs/{job}/reports`: the reports a job
171
  published, with their metrics and context.
172
- `GET /repos/{o}/{n}/flow/runs/{n}/jobs/{job}/report` (`?name=` when the
173
  job published more than one): the report with its files (`manifest`:
174
  path and size), and `…/report/{path}` one file as published.
175
- `GET /repos/{o}/{n}/reports` (`?bookmark=`): the newest run of each.
176
- `GET /repos/{o}/{n}/reports/{name}` (`?id=`, the page's run id, or
177
  `?bookmark=`, default the repository's default bookmark, else wherever
178
  it ran last): one run with its files, and `…/{name}/files/{path}` one
179
  file of it.
180
181
A file over the API comes as published (an HTML page without the frame
182
script), with its type and `Content-Security-Policy: sandbox;
183
default-src 'none'`, so nothing in it runs on the hub's origin. A run
184
whose files expired answers `410` for them; its metrics stay. The errors
185
name what there is: the run's jobs, the job's reports, the report's files.
186
187
- `oh run report RUN JOB` prints the report's `report.toml`; `--list`
188
  shows its metrics, context and files; `--file data/frames.csv` writes
189
  one file to stdout or `-o FILE`; `--name` picks among a job's reports;
190
  `--json` gives the API's answer.
191
- `oh report list [--bookmark B]` and `oh report show NAME [--bookmark B |
192
  --id ID] [--list | --file PATH [-o FILE]]`.
193
194
Still to come: a report's history over the API (`?limit=&before=`) and
195
uploads from anywhere else (`POST /repos/{o}/{n}/reports/{name}`, `oh
196
report upload NAME DIR`, §2).
197
50d93fa4 Andy McCall ↶ 198
## 7. For a project adopting it
199
200
1. Make the test or benchmark job write `out/report/` with a `report.toml`
201
   and, for a page, `index.html` with its libraries bundled (no CDN: the
202
   frame has no network) and its data in `data/`.
203
2. Declare `report "NAME" { path "/src/out/report" when "always" }` in the
204
   job's output.
205
3. Push. The report appears on the Reports tab after the run.
206
f3e5e2bc Andy McCall ↶ 207
### Fitting the hub
208
209
- **Height:** the hub adds a small script to a report's HTML pages
210
  (`/static/report-frame.js`) that tells the page around the frame how
211
  tall the report is, so the frame grows to fit and the report never
212
  scrolls inside it. Lay the page out by its content: nothing sized to the
213
  viewport (`100vh`, `height: 100%` on `html`/`body`), or it grows with
214
  the frame.
215
- **Look:** link the hub's stylesheet, and drop any fonts or colours of the
216
  report's own:
217
218
  ```html
219
  <link rel="stylesheet" href="/static/report.css">
220
  ```
221
222
  It sets the hub's fonts and colour tokens (`--bg`, `--panel`, `--soft`,
223
  `--ink`, `--mute`, `--line`, `--acc`, `--ok`, `--bad`, `--warn`,
224
  `--m1`..`--m5` for series), dark or light as the hub is, switching when
225
  the viewer switches. Charts read them with
226
  `getComputedStyle(document.documentElement).getPropertyValue("--m1")`
227
  and redraw on a `data-theme` change if they cache colours.
228
50d93fa4 Andy McCall ↶ 229
## 8. Order of work
230
c8df33a6 Andy McCall ↶ 231
*Status, 2026-10-11:* storage and index (1) are deployed; the Reports tab,
232
each report's page with its metrics, changes and sparklines, its own page
233
in a sandboxed frame, and checks from thresholds (2) are built, Coverage
234
and Tests listed on the tab while their own pages stay. The frame is
235
sandboxed (`allow-scripts` only) rather than on a second origin: it
236
carries no session, so its files are served under a per-report MAC in the
237
link, with `Content-Security-Policy: sandbox allow-scripts` and nothing
95b525b0 Andy McCall ↶ 238
beyond this hub to load. The timeline (3) is built too: one stop per
239
run on the bookmark, arrow keys to step, Play at 1 to 10 s a run (carried
240
in the URL as `play`), Compare against any other run (`compare`) with
241
every metric's difference and the two pages side by side, and the frame
242
interface: inside the frame, `report.run()` and `report.history("file")`
243
answer from the hub page by postMessage, `history` reading that file of
20b8fe85 Andy McCall ↶ 244
`data/` from each run's capability URL. Retention (§3.3) runs before each garbage
245
collection: every run on the default bookmark keeps its files, and the
246
newest 50 runs of each report on any other bookmark (proposal and tag
247
rules, and a per-repository setting, follow); an expired run keeps its
fac6853c Andy McCall ↶ 248
metrics and summary, and its page says its files expired. *2026-10-28:*
249
reading reports over the API and `oh run report` / `oh report` (4) are
250
built (§6); uploads and history over the API follow.
c8df33a6 Andy McCall ↶ 251
252
50d93fa4 Andy McCall ↶ 253
1. Storage and index: the `report` output kind in OxygenFlow, the runner's
254
   bundle upload, the hub's tree, tables and gc roots; coverage and junit
255
   read through it.
256
2. The Reports tab and kinds; the sandboxed frame; metrics and checks.
257
3. The timeline: scrub, play, compare, and the frame interface.
258
4. `oh report` and the API; this document's §7 as the guide.