Reporting Results
Everything in UniTrack starts with an ingest: a CI job posts its JUnit and JaCoCo reports to the server after a build. This page describes what the endpoint accepts; the CLI & CI Integration and REST API pages cover the wrapper script and the raw HTTP contract.
2. Fields
| Field | Required | Description |
|---|---|---|
|
yes |
Project name; created on first sight. |
|
yes |
One or more test-result files (repeatable): JUnit/Surefire XML, .NET TRX, CTRF JSON, or Go |
|
no |
One or more coverage reports (repeatable): JaCoCo XML, Cobertura XML, LCOV, OpenCover XML, or Go cover profile — auto-detected. |
|
no |
A performance/load-test result — JMeter JTL, k6 JSON, JMH JSON, or Go |
|
no |
Git branch the build ran on. |
|
no |
Commit SHA — used for flaky detection and GitHub status. |
|
no |
Link back to the CI build. |
|
no |
Repository URL (enables GitHub status publishing). |
|
no |
Free-text CI identifier, e.g. |
|
no |
Coverage/component flag — see Coverage Flags. |
|
no |
Stable key to merge sharded runs — see Report Merging. |
Only project and at least one junit or perf file are required; everything else is
optional metadata that unlocks additional features. A perf upload (JMeter JTL) is stored as
a performance run alongside the test/coverage history; the parser computes p50/p90/p95/p99
latency, throughput and error rate (overall and per request label). The response carries a
perfRunId. Upload it from CI with scripts/unitrack-upload.sh --perf results.jtl.
3. What happens on ingest
-
The test results are parsed into suites and cases (format auto-detected — JUnit/Surefire XML, .NET TRX, CTRF JSON, or Go
test -json; see Supported Formats), capturing status and anysystem-out/system-erroutput and attachments. -
The coverage report’s format is auto-detected from its content — JaCoCo XML, Cobertura XML, LCOV (
.info), OpenCover XML, or a Go cover profile — and its counters are aggregated into a line-coverage percentage and stored per file. Thejacocoupload field accepts any of these, so non-JVM projects can report coverage too:Ecosystem Producer → format JVM
JaCoCo (
jacoco.xml)JavaScript
Istanbul/nyc, Jest (
lcov.info)Python
coverage.py (
coverage xml→ Cobertura).NET
Coverlet default (Cobertura) or
--format opencover; OpenCover (<CoverageSession>) -
A
TestRunis created, linked to itsProject, with pass/fail totals. -
Flaky detection compares this run to other runs of the same commit.
-
Triage rules categorise any failures, and similar failures are clustered.
-
The quality gate evaluates the run; if a
repoUrland GitHub token are configured, a commit status is posted to the pull request.
4. Response
201 Created with a JSON body summarising the stored run (id, totals, coverage). A
malformed upload returns 4xx with an error message.
5. Size limits
Multipart uploads default to 25 MB per file and 100 MB per request. Raise these via
spring.servlet.multipart.max-file-size / max-request-size if you upload large merged
reports.
Those caps apply to the compressed request. Because a gzipped report inflates ~10–20×, a second guard caps the decompressed bytes a single part may consume, so an oversized (or gzip-bombed) report fails fast with a clear message instead of exhausting the heap:
| Property | Default | Applies to |
|---|---|---|
|
|
Test + coverage reports (still DOM/full-document parsed, so sized to the heap). |
|
|
Performance results (these stream into a bounded histogram, so this is a generous backstop). |
Set either to 0 to disable that guard. A part that trips the limit is rejected with 4xx
and recorded as a FAILED ingest job whose reason names the limit.
6. Async ingest
Ingest is synchronous by default: the response carries the parsed result (totals, coverage, quality gate) that the CLI and GitHub Action use for the exit code and PR comment.
For large uploads — or when the client doesn’t need the result inline — pass async=true:
curl -F project=myapp -F commit=$SHA -F async=true \
-F '[email protected]' \
http://localhost:8080/api/v1/ingest
# 202 Accepted
# { "jobId": 42, "status": "QUEUED", "statusUrl": "/api/v1/ingest-jobs/42" }
The upload is buffered, queued to a bounded worker pool, and processed off the request thread. Poll the job for the outcome:
curl http://localhost:8080/api/v1/ingest-jobs/42
# { "id": 42, "status": "PROCESSED", "runId": 1234, ... } # or FAILED with failureReason
A single job is readable by an admin or the job’s own uploader. The worker pool and queue
are bounded (unitrack.ingest.async-pool-size / async-queue-capacity); once both are
full a new async upload is rejected with 429. Jobs are persisted, so a job left in flight
by a restart is recovered (marked FAILED) on the next startup.
7. Processing history
Every upload is recorded as an ingest job — PROCESSED or FAILED (with the reason) plus
timing, size and the resulting run id. Admins see the history at /ingest (and
GET /api/v1/ingest-jobs). A failed parse still leaves a record (written in its own
transaction), so a broken CI upload is diagnosable after the fact. Large uploads stream and
aggregate latency into a bounded histogram, so a multi-hour perf log won’t exhaust memory.