Skip to content

What we collect ​

The client sends one JSON payload per batch. This page lists every field. The collector validates payloads against exactly this shape and rejects anything else.

Privacy guarantees ​

  • No user identifiers. No user IDs, account IDs, emails, cookies, or local storage values. The client reads none of them.
  • No IP addresses in stored data. The collector uses the client IP only to rate limit, keeps it in memory for the length of the rate-limit window, and never writes it to storage.
  • No page content. No text, no input values, no element names, no props or state. Areas are identified only by the id strings you choose.
  • No query strings or hashes. Routes come from location.pathname (or your getRoute), never from location.search or location.hash.
  • The session id can't follow a user. It's random per page load (crypto.randomUUID() where available), is never stored on the device, and isn't derived from anything about the user. Reloading the page starts a new session.
  • Unknown fields are rejected. Every object in the collector's schema is strict: a payload with a field that isn't listed here gets a 400 response and isn't stored. This stops a modified client from smuggling extra data into your database.

What you control:

  • Area ids are strings you write. Keep them fixed ("orders-table"), not built from data.
  • Routes: the default getRoute replaces numeric segments with :id, but other dynamic segments (usernames, slugs, emails in paths) pass through. If your paths can contain personal data, pass a getRoute that returns route templates. See Route grouping.
  • appVersion is whatever string you pass.

Example ​

json
{
  "v": 1,
  "session": {
    "id": "6f1c2a7e-4b8d-4f7e-9a51-2d3c8b0e7f14",
    "seq": 1,
    "mode": "lightweight",
    "route": "/orders/:id",
    "appVersion": "2.4.0",
    "startedAt": 1761552000000,
    "sentAt": 1761552184000
  },
  "device": { "memoryGb": 4, "cores": 8, "connection": "4g", "viewport": "390x844" },
  "summary": {
    "renders": 42,
    "slowRenders": 5,
    "longTasks": 2,
    "longTaskMs": 263.4,
    "longFrames": 1,
    "longFrameMs": 212
  },
  "areas": [
    {
      "id": "orders-table",
      "route": "/orders/:id",
      "renders": 42,
      "mounts": 1,
      "slow": 5,
      "totalMs": 391.6,
      "min": 0.6,
      "max": 61.2,
      "p50": 5.7,
      "p75": 10.8,
      "p95": 32.1,
      "hist": [3, 4, 9, 12, 5, 4, 2, 1, 1, 1, 0, 0, 0, 0, 0, 0],
      "worst": [{ "ms": 61.2, "phase": "mount", "at": 412 }, { "ms": 38.9, "phase": "update", "at": 9120 }]
    }
  ],
  "longTasks": [{ "area": "orders-table", "route": "/orders/:id", "count": 2, "totalMs": 263.4, "maxMs": 212 }],
  "longFrames": [{ "area": "orders-table", "route": "/orders/:id", "count": 1, "totalMs": 212, "maxMs": 212 }],
  "dropped": { "areas": 0 }
}

All durations are in milliseconds, rounded to 0.1 ms.

Fields ​

Top level ​

FieldTypeDescription
v1Payload version.
sessionobjectAbout this page load and batch.
deviceobjectCoarse device details.
summaryobjectTotals for the batch.
areasarrayRender timings per area and route.
longTasksarrayLong tasks per area and route.
longFramesarrayLong animation frames per area and route.
dropped.areasnumberArea entries left out to respect maxAreas or maxPayloadBytes.

session ​

FieldTypeDescription
idstringRandom per page load, never stored on the device. Lets the collector group batches from one page load.
seqnumberBatch number within the session, starting at 1.
mode"profiling" | "lightweight"The session's measurement mode when the batch was built. In a profiling session, useRenderMonitor timings are still marks measurements; see Modes.
routestringThe route when the batch was built.
appVersionstring | nullThe appVersion option, or null.
startedAtnumberEpoch milliseconds when the session started.
sentAtnumberEpoch milliseconds when this batch was built.

device ​

Values come from browser APIs. Fields the browser doesn't expose are null; memoryGb and connection are Chromium-only.

FieldTypeDescription
memoryGbnumber | nullnavigator.deviceMemory, which browsers round to a few coarse values.
coresnumber | nullnavigator.hardwareConcurrency.
connectionstring | nullnavigator.connection.effectiveType, e.g. "4g".
viewportstring`${innerWidth}x${innerHeight}` in CSS pixels.

summary ​

FieldTypeDescription
rendersnumberRenders measured across all areas.
slowRendersnumberRenders slower than slowRenderMs.
longTasksnumberLong tasks observed.
longTaskMsnumberTheir total duration.
longFramesnumberLong animation frames observed.
longFrameMsnumberTheir total duration.

areas[] ​

One entry per area id and route.

FieldTypeDescription
idstringThe id given to <RenderMonitor> or useRenderMonitor.
routestringThe route the renders happened on.
rendersnumberMeasured renders (mounts and updates).
mountsnumberHow many of those were mounts.
slownumberRenders slower than slowRenderMs (default 16).
totalMsnumberSum of all render durations.
minnumberFastest render.
maxnumberSlowest render.
p50, p75, p95numberPercentiles, estimated from the histogram and kept within the observed min and max.
histnumber[]Render counts per bucket of HISTOGRAM_BOUNDS, plus a final overflow bucket (16 numbers).
worstarrayThe slowest renders in this batch (up to worstCases, default 5): ms, phase ("mount", "update" or "nested-update"), and at, milliseconds since the session started.

HISTOGRAM_BOUNDS is [1, 2, 4, 8, 12, 16, 24, 33, 50, 75, 100, 150, 250, 500, 1000]: each bucket counts renders up to and including its bound. Histograms from many batches can be added bucket by bucket, which is how the viewer computes percentiles across sessions.

longTasks[] and longFrames[] ​

Main-thread blocks of at least longTaskMs (default 50), from the longtask and long-animation-frame performance entries. Browsers that don't support an entry type send an empty list.

FieldTypeDescription
areastring | nullThe monitored area that was rendering during the block, or null when none was. When several overlap (nested areas), the innermost one that accounts for at least half of the longest overlapping render.
routestringThe route at the time.
countnumberNumber of blocks.
totalMsnumberTheir total duration.
maxMsnumberThe longest one.

Limits the collector enforces ​

Besides rejecting unknown fields, the collector bounds every string and list, so a hostile client can't store unbounded data:

FieldLimit
Area ids1 to 200 characters
RoutesUp to 500 characters
session.id1 to 64 characters
appVersionUp to 100 characters
device.connectionUp to 20 characters
device.viewport<width>x<height>, up to 5 digits each
areas, longTasks, longFramesUp to 1000 entries each
worstUp to 50 entries
histExactly 16 entries
Durations (min, max, percentiles, worst-case ms, maxMs)0 to 3,600,000 ms
Request body65,536 bytes by default (maxBodyBytes)

The client keeps its batches within these limits on its own: it clamps maxPayloadBytes to 1,000–65,536, maxAreas to 1–1000 and worstCases to 0–50, truncates area ids to 200 characters (an empty id is reported as "(unnamed)"), routes to 500 and appVersion to 100. If you lower the collector's maxBodyBytes below the client's maxPayloadBytes, larger batches get a 413.

Released under the MIT License.