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.
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 for the reserved names (network-requests, js-bundle-sizes, react-renders, 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):

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.

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:

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:

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.