pytestHTML Reporter
Home Docs Analytics
The report

Analytics

The Dashboard answers how did this run go, and reads one build to do it. Analytics answers how does this test behave, which no single build can — so it reads every build the report has kept. Nothing extra is collected for it: the archives have been carrying a status per test all along, and had simply never been read across files before.

History that was already on disk

A test that failed once and a test that has failed every night for a fortnight look identical on the Dashboard. They are not the same problem, and telling them apart needs more than one build.

Every build this plugin has written left an output.json beside the report, and each of those carries a status, a failure message, a rerun count and a duration for every test in it. When the next build writes its report, that file is renamed into archive/ and a fresh one takes its place. So a per-test history has been accumulating on disk since the first run — the Analytics tab is what reads it.

output Everything the tab reads, and nothing else
report/
├── pytest_html_report.html
├── output.json                          the build being reported now
└── archive/
    ├── output_1788425529.352927.json     the builds before it, oldest first
    ├── output_1788426203.302967.json
    ├── output_1788426293.530508.json
    └── output_1788426639.262306.json

generate_analytics() runs once per build, and it runs after retention has pruned the archives. The history the tab describes is therefore exactly the history the report ships with — there is no state in which the page talks about builds the file no longer holds.

How the builds are read

  1. Glob and load

    Every archive/*.json is read with json.load. A half-written or hand-edited archive is skipped rather than taking the whole tab down — the other builds still have something to say, and one unreadable file is not a reason to show nothing.

  2. Stamp and sort

    Each build is stamped with the start_time inside the file, falling back to the file's mtime, and sorted on that. The archive's name carries the same kind of number, but as text — where 1788194287.2 sorts after 1788194287.271306.

  3. Append the current run

    output.json goes on the end as the newest build, stamped with its own start_time or, failing that, the current time.

  4. Make the labels distinct

    Axis labels are Sep 03 14:18. A pipeline that runs the suite three times inside a minute produces three builds stamped to the same minute, and three identical ticks on one axis is a chart nobody can read — so repeats are numbered, Sep 03 14:18 (2), rather than given seconds. The seconds are not what anyone is looking for.

What a build is reduced to

Each build is flattened to the handful of things the analytics actually read.

What it buys, and what it costs

The costs are the other side of the same design, and they are worth knowing before you read a number off the tab:

The fixed numbers

Nothing on the tab is tunable. These are the bounds it is built on, and each of them is a readability limit rather than a maths one — the tables behind them still count every build on disk.

ConstantValueWhat it bounds
TREND_BUILDS20Builds the trend charts draw. Forty labels on one axis is unreadable; the tables still read every build kept.
SPARK_BUILDS12Outcomes shown in a test row's History strip.
TOP_SLOWEST10Rows in the slowest-tests chart.
MOVEMENT_NAMES6Test names shown on a movement card before the rest go behind "and N more".
BROKEN_STREAK2Consecutive failing builds before a test counts as consistently failing.
FAULT_TYPES8Exception groups listed in the failure panel; the tail is counted and one click away.
FAULT_NAMES4Test names shown under each exception group.
DURATION_EDGES0.1, 0.5, 1, 5, 10, 30Bucket edges for the duration histogram, in seconds.

The six figures across the top

Six tiles, in this order. Each carries a value, a label and a note that says what the value is made of, because a number with no note is a number people argue about.

TileWhat it isIts note
stability scoreOne number, 0–100, for how much the suite can be trusted. Graded strong ≥ 80 fair ≥ 60 low.pass rate, less how often tests flip
pass rate this run100 × pass / (pass + fail) over the current build's counts, to one decimal. Skips excluded.The drift: no earlier build to compare, level with the last build, or +2.3 pts since the last build.
flaky testsHow many tracked tests have flipped, or needed a retry.flipped or needed a retry
always failingHow many tests have never passed and are two builds into a failing streak.failing 2 builds or more
builds analysedEvery retained build, not the 20 the charts draw.The oldest build's label, oldest Aug 28 09:12.
time in testsThe current build's total test duration.median build 4.5s across every build that recorded one, or not recorded in this run.
The six Analytics tiles: stability score 99, pass rate 98.2% down 1.8 points, 9 flaky tests, 19 always failing, 21 builds analysed and 1m 20s in tests The six Analytics tiles: stability score 99, pass rate 98.2% down 1.8 points, 9 flaky tests, 19 always failing, 21 builds analysed and 1m 20s in tests
The six figures across the top, and the scope line saying how much history they were read from.

The stability score

Two things make a suite untrustworthy and they are not the same thing: tests that fail, and tests that will not say. So the score starts at the mean per-test pass rate and is then charged half of the mean flip rate.

python stability_score()
rated = [h for h in tracked.values() if h['pass_rate'] is not None]

mean_pass = sum(h['pass_rate'] for h in rated) / len(rated)
mean_flip = sum(h['flip_rate'] for h in rated) / len(rated)

score = max(0, min(100, int(round(mean_pass - 50.0 * mean_flip))))

A test alternating pass, fail, pass has a 50% pass rate and tells you nothing, and should not score the same as one that half the team already knows is genuinely broken. Tests with no pass rate at all — only ever skipped — are excluded; with none left the score is unknown and shows --.

Why this run failed

New in 0.4.0The Analytics tab now opens on this panel. A wall of red says how much went wrong; this says what went wrong — the thing you would otherwise get by opening failures one at a time until a pattern appeared.

Every failure in the current build is grouped by the exception it came out of, biggest group first, with the share of the run it holds, the tests in it by name, and how it has moved since the last build.

The names matter as much as the count. 9 TimeoutException says what broke; the names say whether it is one page object nine tests go through or nine unrelated waits, and those are different mornings.

The The
The run’s 28 failures, grouped by exception type — with the tests named under each group and an "and N more" link where the list is long.

What goes into the grouping

Reading the exception out of a message

Nothing new is collected for this. What is stored against a failing test is what pytest printed — and by the time a build has been archived it is text and nothing else. So the type is read back out of the message, in one pass over its lines, with these rules.

  1. ANSI colour is stripped first. It is invisible in the report, but it sits between the start of a line and the exception name, and would stop the name matching at all.
  2. pytest's E marker is stripped. A failure has had it removed by the time it is stored, but an error keeps the whole traceback as printed — and the exception is on one of the marked lines.
  3. A candidate line has to match at its start: an optionally dotted name beginning with a capital, then a colon or the end of the line. ^((?:[A-Za-z_]\w*\.)*[A-Z]\w*)\s*(?::|$). The anchor is the point — an assertion diff prints lines like - ValueError: nope, and that is a string being compared, not something that was raised.
  4. A dotted name is cut to the class. selenium.common.exceptions.TimeoutException and TimeoutException are one group; which of the two a message carries is down to how the traceback was rendered rather than to what went wrong.
  5. A name ending in Error, Exception, Failure, Failed, Skipped, Timeout, Interrupt, Abort or Exit is read as raised, and the last such line wins. A chained failure prints the original traceback first and the exception that actually surfaced last — which is the one pytest itself reports. This suffix rule is also what keeps Selenium's Message: lines out: a WebDriver error prints TimeoutException: Message: ... and then more Message: lines under it.
  6. Any other capitalised name is kept from the first line, where a plain traceback puts its headline.
  7. A bare assert line is read as AssertionError. pytest prints those with no type name at all, and they are far too common a failure to leave sitting in the unclassified pile.
  8. Nothing found puts the failure in Unclassified.
MessageGroupWhy
AssertionError: assert 1 == 2AssertionErrorNamed at the start of the line, and the suffix says it was raised.
assert 1 == 2AssertionErrorThe bare-assert rule.
E   TimeoutException: Message: element not foundTimeoutExceptionThe E marker is stripped; the Message: lines under it carry no raised suffix.
selenium.common.exceptions.TimeoutException: ...TimeoutExceptionThe dotted name is cut to the class.
A chained traceback ending in RuntimeErrorRuntimeErrorThe last raised name wins — the exception that surfaced.
- ValueError: nope (an assertion diff line)UnclassifiedThe pattern is anchored at the start, so the diff marker rules the line out.
Failed: DID NOT RAISEFailedpytest's own pseudo-exception; Failed is in the suffix list.
Free text naming no exceptionUnclassifiedNothing matched.

Ranking, share and movement

Groups are ranked biggest first, with ties broken by name so the order is stable between two runs that failed the same way. Unclassified is held at the bottom however large it grows: a panel headed by Unclassified: 40 has answered nothing.

Each group's share is its count as a rounded percentage of the run's failures. Its movement is its count minus the same group's count in the previous build, and it is worded rather than always signed:

ReadsWhenWhy not just the number
nothing at allThere is no previous build.Not the same thing as a group that has not moved, so it is not drawn the same way.
levelThe count is unchanged.+0 beside a failure count reads as noise.
newThe delta equals the count — the last build had none of these.+3 reads as three more of something that was already there. A failure mode the last build did not have at all is the more interesting of the two.
+2 / -1Everything else.Tinted up or down, so the direction reads before the number does.

The one line the panel is worth reading for

The count on its own is already on the Dashboard. The headline is the second half — where to start — and it is four different sentences rather than one with a suffix:

HeadlineWhen
12 failures, 9 are TimeoutExceptionA lead exists.
all 12 failures are TimeoutException — or the one failure is a TimeoutExceptionOne group is everything.
12 failures, every one a different exceptionNothing groups with anything. Naming the first of twelve one-offs would read as a lead, and that a run failed twelve different ways is itself the finding.
12 failures, none of them naming an exceptionEverything landed in Unclassified.

The panel lists the top eight groups with up to four test names each. Beyond that, the tail is counted rather than droppedand 4 more types, 7 failures between them — and opens in the same dialog as everything else on the tab, with the count of failures in each.

Who owns what

New in 0.4.1One row per owner, worst first. A run with forty failures spread over six teams and a run with forty in one team read identically everywhere else on this page, and they are not the same morning.

The panel is drawn as soon as anything in the run carries @pytest.mark.owner(...), and not at all before that. Each row holds the tests that team owns, the share of the suite that is, their mean pass rate, how many are failing now, how many are flaky and where their minutes go.

Noteowner is written into output.json from 0.4.1 on, which is what lets this panel read across builds at all. Builds archived by an earlier version carry no key and are read as unclaimed rather than as anything invented, so the table fills in as the archive turns over.
The The
Who owns what — one row per owner, and everything nobody claimed collected into Unowned at the foot.

How much it matters

New in 0.4.1One row per severity level, beside Who owns what. Forty failures at trivial and two at blocker are the same number on every other tab and are not remotely the same run.

The rows carry the same figures as the owner table, and the headline over them leads with what somebody came to the tab to find out — 1 critical test failing — rather than with the totals.

Noteseverity is written into output.json alongside owner. Builds archived by an earlier version carry no key and are read as unrated.

Flaky tests

What it means. The test has given two different answers to the same question. It either flipped between passing and failing across builds, or it needed a retry inside a single build.

How it is derived. Every build a test appeared in becomes a point in its history, and the points are summarised:

NumberDerived from
flipsHow many times consecutive decided outcomes differ. pass → fail → pass is two flips.
flip_rateflips / (decided - 1), or 0.0 with fewer than two decided builds. This is what the stability score is charged against.
rerunsThe rerun counts summed across every point — retries inside builds, not across them.
flakyany flips or any reruns — then forced off if the test is broken.

A rerun on its own is enough. It is the least ambiguous flake evidence there is: the same code, the same build, two different answers. That signal only exists if pytest-rerunfailures is installed and retries were budgeted — counting attempts is the only reliable measure available, because --reruns, the ini key and @pytest.mark.flaky(reruns=n) can each set a different one.

NoteSkips are excluded from the pass/fail arithmetic. A skip says nothing about whether a test works, so pass rate, flips and streak are all computed over the builds that decided something. A test skipped for three builds between two passes has not flipped twice.

How to act on it. Open the per-test table, which already opens worst-behaved first, and read the row: its History strip is the last twelve outcomes oldest-first as one block per build, and beside it are the flip count, the retry count and the current streak. A strip that alternates is a race or an ordering dependency; a strip that is solid green with a retry count is something that only fails under load.

The per-test stability table: test names with their verdict pill, History strip, pass rate, builds, flips, retries, current streak and duration The per-test stability table: test names with their verdict pill, History strip, pass rate, builds, flips, retries, current streak and duration
The per-test stability table in its default order — always-failing tests first, each with the History strip of its last builds.

Standing failures

What it means. The test has never passed in any retained build, and it has been failing for at least two builds running. One failure is a failure; a standing one is a different conversation.

How it is derived. A test is broken when it has decided at least once, its fail count equals its decided count, and its streak has reached BROKEN_STREAK (2). The streak is how many decided builds the newest outcome has held for, uninterrupted.

TipA broken test is explicitly un-flagged as flaky. A test that only ever fails is not flaky, it is broken — and listing it under flakiness sends somebody hunting for a race that is not there. It is a bug with an owner.

The same history drives the verdict on every row of the table, and the wording is careful not to claim more than the history supports. Unreliable is a claim about a pattern, and a test that failed the only build it was in has not shown one yet — it has simply failed.

VerdictWhen
Always failingNever passed, and failing two builds or more running.
FlakyFlipped between outcomes, or needed a retry.
FailingPass rate of 0, but not yet a two-build streak.
StablePass rate of 100.
SkippedNo pass rate at all — the test has only ever been skipped and has decided nothing. Shown as --, not as 0, and sorted as -1.
UnreliableEverything else: a pass rate strictly between 0 and 100, with no flips and no retries recorded.
Not in this runOverrides every verdict above when the test's last point is not the current build.

How to act on it. The table's default order is the triage order: consistently failing first, by longest streak then name; then flaky, by flip rate then retry count; then everything else by pass rate, with tests that have decided nothing last. It is sortable, but what it shows before anyone touches it should already be the list to work through.

A test that has decided nothing shows -- for its streak rather than 0 builds, which would read as a measurement rather than as the absence of one.

Pass-rate drift

What it means. Whether the suite is getting better or worse, as a line rather than as today's number.

How it is derived. Each build's rate is computed off that build's own counts, not off the per-test histories: round(100 * pass / (pass + fail), 1), and None when nothing was decided. A None point draws as a gap rather than as a zero — a build in which everything was skipped did not have a pass rate of nought.

Four charts sit under the tiles. Two of them are drawn over the last twenty builds, a third set over the nineteen transitions between them, and the fourth reads the current run alone:

The headline drift on the pass-rate tile compares the current build with the one immediately before it, and is three different sentences rather than one phrase with a suffix: no earlier build to compare, level with the last build when the rounded difference is zero, or +2.3 pts since the last build.

Four charts: pass rate across builds, what moved build to build, where the time goes, and test base growth Four charts: pass rate across builds, what moved build to build, where the time goes, and test base growth
The four trend charts — pass rate, what moved between builds, the duration histogram and how the test base grew.
NoteEvery numeric series is written into the page as an escaped JavaScript literal, and the slowest-tests series — the only one carrying test names — rides on an escaped HTML attribute rather than being written as source code at all. A parametrized case can be called anything at all, and nothing a test is named ever lands somewhere the browser is willing to execute.

Where the run's time goes

What it means. A histogram answers something a slowest-tests list cannot: two thousand tests at 300ms each is a different problem from ten tests at a minute, and both of those suites take ten minutes.

How it is derived. The current run's tests only are spread across seven bands. The first edge a duration is strictly under wins it; anything at or past 30 seconds falls into the overflow bucket, which is the one worth looking at.

BandHolds a duration of
< 100msunder 0.1s
100 - 500ms0.1s up to 0.5s
0.5 - 1s0.5s up to 1s
1 - 5s1s up to 5s
5 - 10s5s up to 10s
10 - 30s10s up to 30s
30s +30s and over

If no test in the current run recorded a duration, both the labels and the values are emitted empty and the chart is not drawn — rather than drawn empty, which reads as a chart that failed to load.

The slowest tests chart takes the current run's tests, drops any whose duration is zero, sorts descending and keeps the top ten. Dropping the zeroes is deliberate: a test that was never timed at all measures nothing, and ten rows of "slowest test: no time at all" reads as a bug rather than as a fast suite. From 0.4.3 a test that was timed keeps six decimal places rather than two — two places of seconds is a 10ms floor, and every unit test quicker than that used to reach the page as a flat 0.0.

Build-level duration is the sum of that build's per-test durations. The time in tests tile shows the current build's total, with the median across every build that recorded one as its note. Durations read as 0.44ms under a hundredth of a second, 820ms under a second, 4.5s under a minute, 12m 04s beyond it, and -- when unknown. The two decimals at the bottom of that ladder are the whole of the figure for a suite of unit tests: rounded to a whole millisecond, a run of them summed to 0ms, which says the run was never timed rather than that it was fast.

CarefulPer-test durations are written into output.json from the release that shipped Analytics onwards. Builds archived by an earlier version have no duration key and are read as not measured — so the duration panels fill from the run that produced them, not retroactively.

How to act on it. Weight in the last two bands is a small number of tests you can name and fix. Weight in the first two bands with a long wall clock is a fixture or a collection problem, not a slow test. The Test Steps tab is where you find out which phase of a named test the time went into.

Screenshot: assets/img/shots/analytics-first-run.png The Analytics tab on a genuine first run at 1440px, light theme — generate by deleting the report folder and running the suite once, with a handful of deliberate failures so the failure panel has something to group. The frame must show the scope line reading "this run only - history builds up as you run again", the six tiles with the pass-rate one noting "no earlier build to compare" and "builds analysed" reading 1, the populated "Why this run failed" panel with no movement chips on any group, and the duration panels — "Where the time goes" and "Slowest tests in this run" — drawn while the trend charts and the movement cards are left out entirely.

The movement cards

What it means. The four numbers a standup asks for: what got fixed, what regressed, what tests were added, and what quietly disappeared.

How it is derived. Every consecutive pair of builds is walked, and each pair produces four lists:

CardListDerived from
Newly failingregressedPresent in both builds, passfail.
Newly fixedfixedPresent in both builds, failpass.
New testsaddedA key in the newer build that the older one did not have.
No longer runremovedA key in the older build that the newer one does not have.

A test that was skipped in either of the two builds is neither fixed nor regressed: only fail → pass and pass → fail count. And a test vanishing from the suite is worth seeing — it is as often an accident as a decision, which is why No longer run is a card of its own rather than a footnote.

All four counts across the recent transitions are what the What moved, build to build chart stacks. The four cards show the latest step only. Each card writes every name it has into the page and shows the first six; the rest are hidden and revealed by the dialog. An empty card says Nothing rather than being hidden — a card that disappears when there is nothing in it makes the row jump about between builds.

On a first build there are no steps at all, and all four cards are empty.

How to act on it. Newly failing is the list to read before anything else on the tab: those tests passed in the build before this one, so the change that broke them is in the diff between the two. No longer run is the one people forget — a test that stopped being collected stops failing, and stops protecting anything.

The "and N more" dialogs

New in 0.4.0Every and N more on the tab — the failure groups, the exception types past the eighth, and all four movement cards — is a button that opens the whole list. The number on its own said how much was being kept from you and offered no way to see it.

The dialog shows the card's own items, not a copy of them. A card renders every name it has and hides the tail with a class; opening the dialog is that hiding taken off. There is no second list in a data attribute to fall out of step with the first, and no test name written into the page twice.

Screenshot: assets/img/shots/analytics-more-dialog.gif A short loop at 1280px, light theme, of one exception group with 40+ tests in it. The motion must show, in order: the pointer clicking "and 37 more"; the dialog opening with the group's name in its header, the count reading "41 tests" and the cursor already in the search box; a few characters typed so the list narrows and the count changes to "3 of 41 tests"; then Escape pressed and the dialog closing. No cuts.

Archives and retention

How far back Analytics reads is whatever retention has kept. The three limits are the only control the tab has, and they are ordinary flags on the run — or, better, keys in the ini file.

The three limits

--archive-count
N default: keep every build

How many builds to keep. It is handled as text rather than as a number, because '' and '0' are different answers and both are asked about later:

  • Unset — no count limit at all.
  • 0 — the whole archive/ directory is removed and the Archives section is not rendered.
  • NN - 1 files are kept on disk. The build being reported now is shown alongside the archived ones and counts against the limit.
--archive-days
D default: none

Keep only builds newer than now - D days. Fractions are accepted — 0.5 is half a day. For a job on a schedule, where "the last 30 days" does not have to be retuned every time the schedule changes.

--archive-since
DATE default: none

Delete every archived build older than this moment — a one-off cut. Accepts YYYY-MM-DD (midnight, local time), YYYY-MM-DD HH:MM and YYYY-MM-DD HH:MM:SS.

How the three intersect

They intersect rather than combine. An archived build has to satisfy every limit you set in order to survive: it must be no older than the cutoff and among the newest keep files. Set none of them and every build is kept for ever.

When --archive-days and --archive-since are both given, the cutoff is the later of the two — the stricter one, so neither can widen the other.

All three are validated by name: a non-numeric or negative --archive-count, a non-numeric or negative --archive-days, or an unparseable --archive-since each fail the run with a usage error rather than raising somewhere inside the render of a build that had already rotated its archive.

shell A nightly job: a month of history, never more than 60 builds
$ pytest tests/ --html-report=./report --archive-days=30 --archive-count=60

# a rolling half day
$ pytest tests/ --html-report=./report --archive-days=0.5

# a one-off cut at a date and a time
$ pytest tests/ --html-report=./report --archive-since='2026-06-01 09:00'

# keep nothing but this build - no Archives section, no Analytics history
$ pytest tests/ --html-report=./report --archive-count=0
TipSet retention in the ini file rather than on the command line. It is a property of the job — every invocation, however it is started, should keep the same window, and a limit that only some of your pipelines pass produces a history whose length depends on who ran it.

How a build is dated

A build is dated by the moment its run started, and that number is kept in the name of its archive file — output_1788426639.262306.json. Retention ages files by reading the name, with the file's mtime only as a fallback.

The name is used rather than the mtime deliberately. A checkout into a fresh CI workspace gives every file the mtime of the clone, so an age limit read from mtimes would decide that a year of history was written this morning and keep all of it for ever. The name survives being copied, zipped, downloaded as an artifact and unpacked somewhere else.

NoteThe stamp in an archive's name is the start time of the build that superseded it — the file is renamed on the way out. The build's own start_time, one build older, is inside the file, and that is what Analytics sorts on.

The size arithmetic

A retained build costs roughly 5KB of the page, and the cost is cumulative: the history is rendered into the one HTML document rather than fetched. That is fine for a suite run a few times a day and it is not fine for one run on a timer.

CadenceBuilds after two monthsHistory in the page
Nightly~60~300KB
Every commit, 20 a day~1,200~6MB
Hourly~1,450~7MB

An hourly run reaches a multi-megabyte report inside a couple of months with nothing set, and the symptom is a report that takes a long time to open rather than an error. Pruning the folder shrinks the page for free.

CarefulRetention is what Analytics reads. Cutting --archive-count to 5 does not only shorten the Archives section — it shortens every pass rate, every flip count and every streak on the tab, permanently, because the builds are deleted from disk.

output.json — the archived build record

output.json sits beside the report and is the only thing that survives from one build to the next. It is written at the end of every build as a single unindented line of JSON.

Four things read it:

Noteoutput.json is not an interchange format, and a sharded run does not merge these. There is no node id in it, no logs, no steps, no attachments and no phases — a merge built on it would produce correct totals over an empty report. The merge command reads shard bundles and writes an output.json of its own, exactly as a pytest run does.

The shape

json output.json, abridged and indented — the real file is one line
{
  "content": {
    "suites": {
      "0": {
        "suite_name": "tests/unit/test_analytics.py",
        "tests": {
          "0": { "status": "PASS", "message": "",
                 "test_name": "test_only_a_failure_counts",
                 "rerun": "0", "duration": 0.0,
                 "owner": [], "severity": "" },
          "1": { "status": "FAIL", "message": "AssertionError: assert 1 == 2",
                 "test_name": "test_totals",
                 "rerun": "2", "duration": 1.24,
                 "owner": ["payments-team"], "severity": "blocker" }
        },
        "status": { "total_pass": 1, "total_fail": 1, "total_skip": 0,
                    "total_error": 0, "total_xpass": 0, "total_xfail": 0,
                    "total_rerun": 2 }
      }
    }
  },
  "coverage": { "percent": 84.12, "statements": 5120, "covered": 4307,
                "missing": 813, "branch": true },
  "date": "September 03, 2026",
  "start_time": 1788426639.262306,
  "total_suite": 30,
  "status": "FAIL",
  "status_list": { "pass": "691", "fail": "1", "skip": "0",
                   "error": "0", "xpass": "0", "xfail": "0",
                   "rerun": "2" },
  "total_tests": "692"
}

Shapes a consumer must not tidy

Some of these are historical, and all of them are load-bearing.

KeyShape
content.suitesKeyed by stringified integer index, not by suite name. Each suite's tests map is keyed the same way, by position within the suite.
tests.<j>.rerunA string. duration beside it is a number.
tests.<j>.durationWritten for the sake of the build after this one: Analytics reads durations back out of the archives, and a number that was never stored is a number no later run can show.
tests.<j>.ownerA list, in the order the markers were written, and empty for a test nobody claimed. New in 0.4.1, and written for the same reason as duration: the owner roll-up is a cross-build table, so ownership has to be in the file before it can be read across files.
tests.<j>.severityOne string, already resolved between markers, or "" for a test nobody rated. New in 0.4.1. A build archived before it existed has no key, which reads the same way.
tests.<j>.messageThe failure text, stored raw. This is what the exception grouping reads.
status_listRun-wide totals, and every value is a string. So is total_tests. The Trends chart's "Failed" series is fail + error.
status"FAIL" if any suite recorded a non-zero total_fail or total_error; "PASS" otherwise.
coveragePresent only when coverage was measured. Its absence means "not known", which is a different answer from zero and is the true one.
start_timeThe moment the build is filed under. It names the file the next build archives this one as, labels the trend point, and orders the builds in Analytics.
date"%B %d, %Y" — the day the tests ran. Both the Trends loader and the Archives loader parse this back out.

Delta vs the previous build

The archives pay for one more thing, and it is not on the Analytics tab at all. Once there is a build to compare against, the Dashboard's Highlights card gains a second entry saying which way the suite is moving — ▲ +3 failures under SINCE LAST BUILD, red when there are more failures than last time and green with a when there are fewer.

The absolute count tells you how bad this build is. The delta tells you whether it is getting better, which is the one you act on.

Where to go next