Hydration Proof

Search documentation

Find a page or section

Reports: terminal, HTML, JSON, JUnit, SARIF

One run, the same findings, in the format each tool reads.

hydration-proof writes each hydration error report (SARIF, JUnit, JSON, HTML, GitHub annotations, GitLab Code Quality) from the same findings, into .hydration-proof/report/. Choose them with --reporter or the reporters option. Every format carries the stable issue codes and fingerprints, so CI tools recognize the same problem from one run to the next.

Which hydration error report: SARIF, JUnit or JSON?

ReporterWritesUse it for
listThe terminalReading results as the run goes
jsonreport.jsonScripts, merging shards, the Node API
htmlreport.html and screenshots/Debugging a finding
junitjunit.xmlTest results in any CI system
sarifreport.sarifGitHub code scanning and other static-analysis dashboards
githubNothing: workflow commandsAnnotations and the job summary on GitHub Actions
gitlabgl-code-quality.jsonThe Code Quality widget in GitLab merge requests
npx hydration-proof test --reporter list,html,json,junit,sarif

When reporters is not set, the default is list, json and html. On GitHub Actions (GITHUB_ACTIONS=true), github is added automatically unless you pass --reporter. hydration-proof init writes reporters: ['list', 'json'] into the config it creates. Reports go to .hydration-proof/report/; change it with outputDir or --output. Running in CI has the reporter set for each CI system.

Terminal (list)

One line per page while testing. Failing pages show their most important findings with the element, the server and client values, the likely cause, the source location and the first fix. A summary follows:

  ✖ /dashboard 1.1s  1 error
    HP1001 Text differs between server and client  (timezone difference, 95%)
      #last-login  in LastLogin
      server: "Signed in at 5:00 AM"
      client: "Signed in at 10:00 AM"
      app/dashboard/LastLogin.tsx:14:10
      → Pass an explicit timeZone to the formatter (the same on both sides), or format the date after mount.

JSON (json)

report.json contains everything: run information, a summary, every page and every issue. It is what the other tools build on.

  • Schema. The format is described by node_modules/hydration-proof/schema/report.json (its $id is https://hydration.jscrate.dev/schema/report.json). Within schemaVersion: 1 it only changes in compatible ways: fields are added, never removed.
  • Fingerprints. Every issue has a fingerprint that stays the same across runs as long as the problem is the same: the same code, route pattern, element and attribute. Ignore rules and baselines use it. fingerprintVersion says how the fingerprints were computed.
  • CI data. Findings carry their owners (owners), whether the baseline knows them (baseline, new) and, in a monorepo, their project. The run records the git commit and branch. With ci.history, history holds the previous runs.
  • Pages. Each page records where its route came from (source: config, discovered, manifest, sitemap, crawl or not-found) and, when the tool started the app, the error and warning lines the server printed while the page loaded (serverLogs).
  • Redaction. Values removed by redaction are counted in summary.redacted.

HTML (html)

report.html is a single file you can open locally or keep as a CI artifact. Its Content Security Policy starts with default-src 'none', and the page makes no network requests. Screenshots are image files in screenshots/ next to it, so keep the folder together.

It shows:

  • a summary and filters: page status, severity, cause, kind, scenario, owner, text search and ignored findings;
  • per page, the findings with server and client values side by side (differences highlighted), the element on each side, the component, the source code, evidence and fixes, and the server log lines;
  • screenshots of the hydrated page and of the server HTML without scripts, with the affected elements outlined;
  • a timeline of the page: scripts and RSC requests loading, React loading, hydration commits for the root and each Suspense boundary, React errors, scripts changing the DOM, streamed content and the navigation check;
  • the other environments of a route for matrix runs, what each probe changed, and flaky findings for repeated runs;
  • with ci.history, the errors and warnings of the last 30 runs.

screenshots sets which pages get screenshots: 'failures' (the default with the HTML reporter), 'all' or 'off'. Deep links such as report.html#page=…&issue=… open one finding.

JUnit XML (junit)

junit.xml works with any CI system that reads JUnit results, including GitLab, CircleCI, Jenkins, Azure Pipelines and Buildkite.

  • One <testsuite> per scenario, or per scenario and build mode with --mode both.
  • One <testcase> per page. The classname is hydration-proof.<scenario>[.<mode>] and the name is the page path with its query string. A path tested twice in one suite gets (2), (3) and so on, so every pair is unique. time is the page's test time in seconds.
  • A failed page has a <failure>. Its type is the first issue code, its message lists the error codes, and its body lists each error with the element, both values, the likely cause, the source location, the first fix and the documentation link.
  • A page that could not be tested has an <error> whose type is the outcome.
  • A page with only warnings or info findings passes. Its findings go to <system-out>, with the URL, the outcome and any ignored findings. Server log lines go to <system-err>.

Test cases follow the page status and ignore --fail-on and ci.maxWarnings. With --fail-on warning the job fails while pages with only warnings still pass in JUnit. The exit code always applies the policy.

Values are quoted with escapes such as \n and \u0000, so invisible differences stay visible.

SARIF (sarif)

report.sarif is a SARIF 2.1.0 log for GitHub code scanning and other static-analysis dashboards.

  • Every issue code is a rule. Its id is the code (HP1001) and its name is the PascalCase issue name (TextMismatch), with the description, the fix and a documentation link. The default level follows the severity: error, warning, or note for info.
  • Each finding is one result, with the source file and line, the route pattern, scenario and build mode as a logical location, and the fingerprint in partialFingerprints["hydrationProof/v1"], so code scanning recognizes an alert across runs.
  • Ignored findings are included with an external suppression, so no alert opens for them.
  • invocations[0].executionSuccessful is false only when the tool itself could not run (exit codes 2, 3, 4 and 70).

To upload it to GitHub code scanning, give the job the security-events: write permission and use github/codeql-action/upload-sarif:

.github/workflows/hydration.yml
permissions:
  contents: read
  security-events: write
 
steps:
  # checkout, setup-node, npm ci, install
  - run: npx hydration-proof test --reporter list,html,json,github,sarif
  - uses: github/codeql-action/upload-sarif@v3
    if: ${{ !cancelled() && hashFiles('.hydration-proof/report/report.sarif') != '' }}
    with:
      sarif_file: .hydration-proof/report/report.sarif
      category: hydration-proof

Code scanning is on for public repositories; private ones need GitHub Advanced Security. Pull requests from forks get a read-only token, so the upload step fails there.

GitHub Actions (github)

The github reporter prints workflow commands and writes no file.

  • Annotations. Each problem becomes an ::error, ::warning or ::notice annotation, with the page, scenario, element, both values (up to 200 characters each), the likely cause and its confidence, the first fix and the documentation link. When the source location is known, it is attached to that file and line, so it shows in the pull request's changed files. The same fingerprint in several scenarios, modes or URLs of one route becomes one annotation.
  • Limits. GitHub shows at most 10 annotations of each level per step. The most important come first, and a final notice says how many were left out.
  • Job summary. A Markdown summary with a pass or fail headline, totals, the pages with problems and a section per problem. GitHub rejects summaries over 1 MiB, so long ones are shortened, with a note, to stay under that size.

GitLab Code Quality (gitlab)

gl-code-quality.json is a Code Quality report that GitLab shows in merge requests. It has one entry per finding that is not ignored:

FindingGitLab severity
Error with a confidence of 0.9 or morecritical
Any other errormajor
Warningminor
Infoinfo

check_name is the issue code, and fingerprint is unique within the file and stable across runs. Declare the file, and the JUnit file, as reports:

.gitlab-ci.yml
artifacts:
  when: always
  paths:
    - .hydration-proof/report
  reports:
    codequality: .hydration-proof/report/gl-code-quality.json
    junit: .hydration-proof/report/junit.xml

File paths in CI formats

Paths are relative to the repository root: the closest directory above the app that contains .git, or GITHUB_WORKSPACE for GitHub annotations and CI_PROJECT_DIR for GitLab. A source file in another workspace package of a monorepo is still linked. A finding with no source location, or one outside the repository, is attached to line 1 of the config file (or of package.json) in SARIF and GitLab, and the message names the real source.

Source locations

Findings point at the code that rendered the element when that can be proven:

  • Development builds (--mode development): the exact file and line where the element was created.
  • Production builds with browser source maps: the component that rendered the element, found in the loaded scripts and mapped back through the source maps. In Next.js, turn them on:
next.config.ts
import type { NextConfig } from "next";
 
const nextConfig: NextConfig = {
  productionBrowserSourceMaps: true,
};
 
export default nextConfig;

Otherwise the finding says why there is no location, in sourceUnavailableReason: for example, the scripts have no source maps, or only framework code could be mapped (elements rendered by Server Components have no client code).