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 type | What it captures |
|---|---|
network-requests | Every fetch / XHR issued during replay |
js-bundle-sizes | JavaScript bundles loaded during replay, broken down by source file |
react-renders | Cumulative React commit count, sampled at each screenshot |
react-component-renders | Per-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 examplescreenshot-after-event-00012orfinal-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 torecordCustomSnapshot. 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 examplenetwork-requests), so you can filter by it.sessionDescription— a short, human-readable summary of what the user was doing in that session (for exampleAdded an item to the cart), generated by Meticulous and handy for labelling sessions in your report. It isnullwhen 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:
| Field | Description |
|---|---|
url | Request URL |
method | HTTP method |
requestHeaders | Request headers (HAR shape) |
requestBody | Request body, if any |
status | Status code of the stubbed response served during replay, or null if the request was not matched to a recorded request |
responseHeaders | Response headers (HAR shape) |
responseBody | Response body, if any |
matched | true 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:
| Field | Description |
|---|---|
url | Resolved URL of the bundle |
sizeInBytes | Decoded (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). |
status | HTTP status code of the served response |
sourceBreakdown | Optional. 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:
| Field | Description |
|---|---|
script | The 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. |
bytes | Generated (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:
| Field | Description |
|---|---|
commits | Cumulative 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:
| Field | Description |
|---|---|
components | Per-component cumulative render counts by this stage, most-active first and capped to the busiest components |
Each components entry:
| Field | Description |
|---|---|
name | The component's display name (e.g. UserMenu), or null when the name was minified away by your production build |
source | Original source location of the component as <path>:<line>:<col> (resolved from your source maps), or null when it could not be resolved |
commits | Cumulative 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 }
]
}
}