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.

1. The endpoint

POST /api/v1/ingest
Content-Type: multipart/form-data

2. Fields

Field Required Description

project

yes

Project name; created on first sight.

junit

yes

One or more test-result files (repeatable): JUnit/Surefire XML, .NET TRX, CTRF JSON, or Go test -json — auto-detected. See Supported Formats.

jacoco

no

One or more coverage reports (repeatable): JaCoCo XML, Cobertura XML, LCOV, OpenCover XML, or Go cover profile — auto-detected.

perf

no

A performance/load-test result — JMeter JTL, k6 JSON, JMH JSON, or Go test -bench (auto-detected): latency percentiles, throughput, error rate.

branch

no

Git branch the build ran on.

commit

no

Commit SHA — used for flaky detection and GitHub status.

buildUrl

no

Link back to the CI build.

repoUrl

no

Repository URL (enables GitHub status publishing).

ciProvider

no

Free-text CI identifier, e.g. github-actions.

flag

no

Coverage/component flag — see Coverage Flags.

runKey

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

  1. 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 any system-out/system-err output and attachments.

  2. 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. The jacoco upload 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>)

  3. A TestRun is created, linked to its Project, with pass/fail totals.

  4. Flaky detection compares this run to other runs of the same commit.

  5. Triage rules categorise any failures, and similar failures are clustered.

  6. The quality gate evaluates the run; if a repoUrl and 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

unitrack.ingest.max-report-bytes

256MB

Test + coverage reports (still DOM/full-document parsed, so sized to the heap).

unitrack.ingest.max-perf-bytes

1GB

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 jobPROCESSED 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.