> Meticulous records real user sessions from a web app and replays them against each commit to catch visual regressions.
>
> Full AI-readable docs index (complete project setup journey + every page as markdown): https://app.meticulous.ai/llms.txt

# 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](/docs/custom-checks/recording-custom-data) and compares base vs head per session.

> **info: Contact Meticulous to enable snapshots**
> Built-in snapshots are not enabled by default. Reach out to the Meticulous team at [support@meticulous.ai](mailto: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-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](/docs/custom-checks/recording-custom-data).

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`](https://www.npmjs.com/package/@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.

```typescript
const { baseSnapshots, headSnapshots } = await getSnapshotsFromTestRun({
  client,
  testRunId,
  snapshotTypes: ["network-requests", "js-bundle-sizes", "react-component-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:**

```json
{
  "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:**

```json
{
  "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-component-renders`

**Snapshot type:** `react-component-renders`

**When it is captured:** Meticulous installs a React DevTools-style hook before your app's `react-dom` initializes. On each React commit it walks the committed 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. 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 high `commits` value. What matters for a regression is the **delta vs base**: a component whose `commits` jumps on head pinpoints *which* component is responsible — for example from a missing memoization or an unstable prop or context value.

**Example entry:**

```json
{
  "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 }
    ]
  }
}
```
