> 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

# Recording custom snapshots

Built-in custom checks use snapshots Meticulous captures automatically during replay (for example `network-requests`). When you need to check a metric Meticulous does not collect for you — CPU pressure, render timing, memory usage, or any other JSON-serializable signal — record it yourself from browser code during replay with `window.Meticulous.replay.recordCustomSnapshot(...)`.

Your custom check reporter then downloads those snapshots (alongside built-in ones) with `getSnapshotsFromTestRun` and compares base vs head.

## Prerequisites

- Custom checks enabled for your project. Contact the Meticulous team to enable the custom checks.

## The `recordCustomSnapshot` API

During a replay, call `window.Meticulous.replay.recordCustomSnapshot` with:

- `snapshotType` — a stable name for this kind of snapshot (you will request the same type in your reporter).
- `data` — a JSON-serializable payload (objects, arrays, strings, numbers, booleans, or `null`).
- `versionNumber` (optional) — increment when you change the shape of `data`; Meticulous can surface version mismatches between base and head in the UI.

```typescript
const result = window.Meticulous.replay.recordCustomSnapshot({
  snapshotType: "my-metric",
  data: { value: 42, unit: "ms" },
  versionNumber: 1,
});

if (!result.success) {
  // Custom snapshotting is disabled for this project, or the snapshot was dropped
  // (see "When recording is a no-op" below).
}
```

### Snapshot type names

Choose a `snapshotType` that:

- Matches `/^[a-z0-9-]{1,64}$/` (lowercase letters, digits, and hyphens only).
- Does **not** collide with built-in Meticulous types — see [Built-in snapshot types](/docs/custom-checks/built-in-snapshot-types) for the reserved names (`network-requests`, `js-bundle-sizes`, `react-component-renders`, `custom-recording`).

Use one type per metric family — for example `pressure-observer-cpu-read` for CPU pressure readings, not a new type on every call.

### When each snapshot is taken

Every snapshot is tagged with a `stageDuringSession` — the screenshot in the session timeline that follows the recording (for example `screenshot-after-event-00012` or `final-state`). This lets you align snapshots with visual diffs when debugging a check.

Call `recordCustomSnapshot` from:

- Your app code at any point during replay (for example when an observer fires).
- A listener registered with `addOnBeforeScreenshotListener` — snapshots are tagged with the screenshot about to be taken.
- A listener registered with `addOnReplayCompletionListener` — snapshots are tagged with `final-state`.

Listeners are useful when you want a consistent sampling point (for example, capture a metric before each comparison screenshot):

```typescript
window.Meticulous.replay.addOnBeforeScreenshotListener(({ stageDuringSession }) => {
  window.Meticulous.replay.recordCustomSnapshot({
    snapshotType: "my-metric",
    data: { stageDuringSession, value: measureSomething() },
  });
});
```

Listeners must finish quickly — they run on the screenshot critical path and are bounded by a real-time timeout. Prefer synchronous work or short microtasks; avoid waiting on stubbed timers.

## When recording is a no-op

`recordCustomSnapshot` returns `{ success: false }` (and does not throw) when:

- Custom snapshot recording is **not enabled** for your project.
- The page is **not** running as a Meticulous replay (`window.Meticulous.isRunningAsTest` is false).

Invalid input (bad `snapshotType`, non-JSON-serializable `data`, or `undefined` data) **throws** so you can catch misconfiguration during development.

## Example: sampling JS heap memory during replay

A good real-world use is tracking **JavaScript heap memory** so a check can warn when a change makes a flow noticeably heavier. This mirrors how we sample CPU pressure in our own app's [custom checks](/docs/custom-checks/writing-a-custom-check).

Many performance APIs are stubbed during replay, so read real values through `window.Meticulous.replay.native`. The example below samples `performance.memory.usedJSHeapSize` (Chromium) on an interval and records each reading as a `js-heap-memory` snapshot:

```typescript
const JS_HEAP_MEMORY_SNAPSHOT_TYPE = "js-heap-memory";
const SAMPLE_INTERVAL_MS = 1_000;

const noop = () => {
  /* nothing was started */
};

const startRecordingJsHeapMemory = (): (() => void) => {
  if (typeof window === "undefined") {
    return noop;
  }

  // `window.Meticulous` is a discriminated union on `isRunningAsTest`; the
  // `replay` API only exists in the running-as-test variant.
  const meticulous = window.Meticulous;
  if (!meticulous?.isRunningAsTest) {
    return noop;
  }
  const { replay } = meticulous;

  // `native` exposes the real (non-stubbed) performance metrics. `memory` is
  // only present on Chromium.
  const { memory } = replay.native.performance;
  if (!memory) {
    return noop;
  }

  const record = () => {
    replay.recordCustomSnapshot({
      snapshotType: JS_HEAP_MEMORY_SNAPSHOT_TYPE,
      data: {
        usedJSHeapSize: memory.usedJSHeapSize,
        totalJSHeapSize: memory.totalJSHeapSize,
        time: replay.native.performance.now(),
      },
      versionNumber: 1,
    });
  };

  // `native.setInterval` is the real wall-clock timer captured before stubbing,
  // so sampling runs on real time rather than the replay's frozen virtual time.
  record(); // initial sample
  const interval = replay.native.setInterval(record, SAMPLE_INTERVAL_MS);

  return () => replay.native.clearInterval(interval);
};
```

Wire `startRecordingJsHeapMemory()` into your app bootstrap so it runs during Meticulous test replays (and is a no-op in production and in browsers without `performance.memory`).

## Using recorded snapshots in a custom check

In your CI reporter, include your `snapshotType` when downloading snapshots for the test run:

```typescript
const { baseSnapshots, headSnapshots } = await getSnapshotsFromTestRun({
  client,
  testRunId,
  snapshotTypes: ["js-heap-memory", "network-requests"],
});
```

Each snapshot includes `sessionId`, `sessionDescription` (a short, human-readable summary of what the user was doing in the session, or `null` when none exists), `type`, `stageDuringSession`, `data`, and optionally `versionNumber`. Compare base vs head per session using the same patterns as [Writing a custom check](/docs/custom-checks/writing-a-custom-check).
