Built-in snapshot types

Meticulous captures some snapshot types automatically during replay — no application code required. Your custom check reporter downloads them alongside any snapshots you record yourself and compares base vs head per session.

Contact Meticulous to enable snapshots

Built-in snapshots are not enabled by default. Reach out to the Meticulous team at support@meticulous.ai to turn on the snapshot types you need for your project.

Snapshot types Meticulous collects for you

Snapshot typeWhat it captures
network-requestsEvery fetch / XHR issued during replay
js-bundle-sizesJavaScript bundles loaded during replay, broken down by source file
react-rendersCumulative React commit count, sampled at each screenshot
react-component-rendersPer-component breakdown of which React components re-rendered and how often, sampled at each screenshot

These are the only built-in data snapshot types today. Meticulous also recognizes custom-recording as a reserved name — that is a project capability flag that enables recordCustomSnapshot, not a snapshot file you download. See Recording custom snapshots.

Do not use these names for your own snapshotType values when calling recordCustomSnapshot.

Common snapshot shape

Every snapshot — built-in or customer-recorded — is a JSON object with:

  • stageDuringSession — which comparison screenshot in the session timeline this entry belongs to (for example screenshot-after-event-00012 or final-state). Meticulous tags each entry with the next screenshot taken after the request or bundle load, so you can group data per visual diff stage when debugging a check.
  • data — the payload for that entry (schema depends on the snapshot type).
  • versionNumber (optional) — only present on customer-recorded snapshots when you pass one to recordCustomSnapshot. Built-in snapshots omit this field.

Snapshots for a test run are stored per replay under custom-checks-snapshots/ — one uncompressed .json file per type (for example network-requests.json). Your reporter downloads them via getSnapshotsFromTestRun from the @alwaysmeticulous/custom-checks SDK; you do not read these files from S3 directly.

When you call getSnapshotsFromTestRun, each returned snapshot also includes:

  • sessionId — the session the snapshot was captured in, so you can align the same session on base and head.
  • type — the snapshot kind (for example network-requests), so you can filter by it.
  • sessionDescription — a short, human-readable summary of what the user was doing in that session (for example Added an item to the cart), generated by Meticulous and handy for labelling sessions in your report. It is null when the session has no description (for example older sessions, or sessions that have not been selected for testing), so treat it as optional.
const { baseSnapshots, headSnapshots } = await getSnapshotsFromTestRun({
  client,
  testRunId,
  snapshotTypes: ["network-requests", "js-bundle-sizes", "react-renders"],
});

network-requests

Snapshot type: network-requests

When it is captured: Every fetch or XHR the page issues during replay. Each request becomes one array entry, tagged with the session stage active when the request was made.

What each entry contains:

FieldDescription
urlRequest URL
methodHTTP method
requestHeadersRequest headers (HAR shape)
requestBodyRequest body, if any
statusStatus code of the stubbed response served during replay, or null if the request was not matched to a recorded request
responseHeadersResponse headers (HAR shape)
responseBodyResponse body, if any
matchedtrue if the request was matched and stubbed; false if it was left unmatched

Large request and response bodies follow the replay timeline's truncation rules: oversized bodies are ellipsized with an MD5 of the remainder so content changes remain detectable without storing the full payload.

Example entry:

{
  "stageDuringSession": "screenshot-after-event-00003",
  "data": {
    "url": "https://app.example.com/api/graphql",
    "method": "POST",
    "requestHeaders": [{ "name": "content-type", "value": "application/json" }],
    "requestBody": "{\"query\":\"...\"}",
    "status": 200,
    "responseHeaders": [{ "name": "content-type", "value": "application/json" }],
    "responseBody": "{\"data\":{...}}",
    "matched": true
  }
}

js-bundle-sizes

Snapshot type: js-bundle-sizes

When it is captured: Every time a JavaScript bundle finishes loading during replay. As well as script resources, this includes JavaScript loaded via <link rel="modulepreload"> or prefetch — any .js / .mjs / .cjs URL — which the browser does not classify as a script. The same URL loaded twice in one session yields two entries before deduplication (see below).

What each entry contains:

FieldDescription
urlResolved URL of the bundle
sizeInBytesDecoded (uncompressed) size of the served bundle body, in bytes. -1 when the body could not be retrieved (for example a response served from the browser cache).
statusHTTP status code of the served response
sourceBreakdownOptional. Per-source-file breakdown of the bundle's bytes (see below). Omitted when the bundle could not be attributed back to its sources.

Sizes are measured Node-side from the served response, not from the browser Performance API. During replay all responses are intercepted, so the browser reports zero transfer sizes for them. The size is always the decoded body length — the content-length header is deliberately ignored so a bundle is sized identically whether it happens to be served compressed or uncompressed, keeping the metric stable across replays.

sourceBreakdown: When Meticulous can load a bundle's source map, the entry also carries a sourceBreakdown — the bundle's generated (minified) bytes attributed back to the original source files they were compiled from. This lets a check link a bundle's size to the source responsible for it, and — because source paths are stable across builds whereas content-hashed bundle URLs are not — makes base-vs-head size diffs meaningful per source file. The list is sorted largest-first and capped to the 100 biggest sources per bundle.

Each sourceBreakdown entry:

FieldDescription
scriptThe original source file the bytes came from (for example src/components/Foo.tsx, or a node_modules/... dependency). Falls back to the bundle url for generated runtime/wrapper code that had no source mapping.
bytesGenerated (minified) bytes of the bundle attributed to script via the source map. Summed across the breakdown these approximate the bundle's uncompressed size.

sourceBreakdown is omitted when source-map attribution was not possible — for example no source map is available for the bundle, or source-map loading is disabled for the project — leaving just url, sizeInBytes and status.

Deduplication: Within the same stageDuringSession, Meticulous deduplicates by URL — for example when a chunk is both preloaded and executed in the same stage. When duplicates occur, the largest reported sizeInBytes is kept (a cache-served load may report -1 while the full transfer reports the real size).

Example entry:

{
  "stageDuringSession": "final-state",
  "data": {
    "url": "https://app.example.com/_next/static/chunks/main-abc123.js",
    "sizeInBytes": 184320,
    "status": 200,
    "sourceBreakdown": [
      { "script": "node_modules/react-dom/cjs/react-dom.production.min.js", "bytes": 118500 },
      { "script": "src/components/Dashboard.tsx", "bytes": 24310 },
      { "script": "https://app.example.com/_next/static/chunks/main-abc123.js", "bytes": 9200 }
    ]
  }
}

react-renders

Snapshot type: react-renders

When it is captured: Meticulous installs a React DevTools-style hook before your app's react-dom initializes and counts React commits (render → DOM cycles) as the replay runs. At each comparison screenshot it records the cumulative commit count so far, producing one entry per screenshot stage. This requires no application code and is a no-op on non-React pages (the count stays 0).

What each entry contains:

FieldDescription
commitsCumulative number of React commits (render → DOM cycles) by this stage of the session

The count is cumulative and monotonic across the whole session (it is carried across full-page navigations), so the maximum commits across a session's entries is the total number of commits in the session, and the difference between consecutive stages is the number of commits caused by that interaction. A materially higher count on head vs base is a signal of a rendering regression — for example a new render loop, or an effect that schedules extra state updates. To see which components re-rendered within those commits — for example from a missing memoization or an unstable prop or context value — use the react-component-renders snapshot below.

Example entry:

{
  "stageDuringSession": "screenshot-after-event-00007",
  "data": {
    "commits": 142
  }
}

react-component-renders

Snapshot type: react-component-renders

When it is captured: Like react-renders, but on each commit the engine additionally walks the committed React fiber tree and attributes the work to the components that actually re-rendered (React's PerformedWork flag), recording a per-component breakdown at each comparison screenshot. It is a heavier companion to react-renders and is enabled separately. No application code required; a no-op on non-React pages.

What each entry contains:

FieldDescription
componentsPer-component cumulative render counts by this stage, most-active first and capped to the busiest components

Each components entry:

FieldDescription
nameThe component's display name (e.g. UserMenu), or null when the name was minified away by your production build
sourceOriginal source location of the component as <path>:<line>:<col> (resolved from your source maps), or null when it could not be resolved
commitsCumulative number of commits in which this component re-rendered, by this stage

Use source (not name) to align components across base and head: production builds often minify names differently between builds, but the resolved source path is stable. Each entry counts one render per instance, so a component rendered in many places (a list row, an icon) can have a commits value larger than the session's total react-renders count, and the per-component counts do not sum to it. What matters for a regression is the delta vs base: a component whose commits jumps on head pinpoints which component is responsible.

Example entry:

{
  "stageDuringSession": "screenshot-after-event-00007",
  "data": {
    "components": [
      { "name": "ResultRow", "source": "src/search/ResultRow.tsx:11:0", "commits": 220 },
      { "name": "ResultsList", "source": "src/search/ResultsList.tsx:24:0", "commits": 38 },
      { "name": null, "source": "node_modules/some-lib/Tooltip.js:8:0", "commits": 9 }
    ]
  }
}