Capture format
Every Faylee recording, instant replay and screenshot stores what was captured alongside the media as one JSON file, data.json. Its shape is called a capture bundle. This page describes version 2 of the format for anyone who reads bundles: scripts, agents, importers and test generators.
Get a recording’s bundle with GET /recordings/:id/capture. You upload one in step 3 of an upload.
Top-level fields
Section titled “Top-level fields”| Field | Type | Notes |
|---|---|---|
version | 2 | Always the number 2 for this format |
kind | string | video, replay or screenshot |
startedAt | string | ISO 8601. Wall-clock start of the timeline |
durationMs | number | Length of the timeline in milliseconds, pauses excluded. 0 or more |
environment | object or null | The browser and device. See Environment |
pages | object[] | Pages open during the capture. See Pages |
console | object[] | Console messages. See Console |
errors | object[] | Uncaught errors and unhandled rejections. See Errors |
network | object[] | Requests, sorted by at. See NetworkEntry |
actions | object[] | What the user did. See UserAction |
dom | object[] | rrweb events for the page replay. See DOM events |
vitals | object[] | Web Vitals and runtime measurements. See Vitals |
custom | object[] | Metadata the page attached. See Custom metadata |
sockets | object[] | WebSocket and EventSource connections. See SocketConnection |
linkedSessions | object[] | The same visit in other tools. See Linked sessions |
edits | object[] | Changes the reporter made to the page. See Edits |
annotations | object[] | Marks the reporter drew. See Annotation |
dom, vitals, custom, sockets, linkedSessions, edits and annotations were added to version 2 over time. Bundles made before them may not have the key at all. Treat a missing key as an empty list.
Not every kind fills every list. A recording sent through a recording link records the screen rather than a page, so it has an environment and at most one page, and every other list is empty.
{ "version": 2, "kind": "video", "startedAt": "2026-09-30T14:02:23.120Z", "durationMs": 48200, "environment": { "userAgent": "Mozilla/5.0 ...", "browser": { "name": "Chrome", "version": "131.0.6778.86" } }, "pages": [{ "at": 0, "url": "https://shop.example.com/checkout", "title": "Checkout" }], "console": [], "errors": [], "network": [], "actions": [], "dom": [], "vitals": [], "custom": [], "sockets": [], "linkedSessions": [], "edits": [], "annotations": []}The environment above is shortened.
Almost every entry has at: milliseconds into the recording’s timeline, never negative. 0 is the moment the timeline starts, and durationMs is where it ends.
The timeline skips pauses. When the reporter pauses a video recording and resumes it, the paused time is not on the timeline, and entries captured during it are dropped. So at lines up with the video, and an entry at at: 12400 is about 12.4 seconds into it.
To get the wall-clock time of an entry, add at to startedAt. This is exact only when the recording was never paused before that entry. The bundle does not record where pauses were.
Instant replays and screenshots are never paused. Their timeline starts at the first event inside the captured window.
Some things describe the page rather than a moment, such as the page that was open when recording started, load metrics and app metadata. When they happened before the timeline started, they are placed at at: 0.
DOM events are the one exception to at. See DOM events.
| Field | Type | Notes |
|---|---|---|
at | number | When the page loaded. The page open when recording started is at 0 |
url | string | |
title | string |
Console
Section titled “Console”| Field | Type | Notes |
|---|---|---|
at | number | |
level | string | log, info, warn, error, debug or trace |
args | array | The arguments passed to the console call, as JSON values |
stack | string or null |
Values that are not plain JSON are converted. Maps become { "[[Map]]": [[key, value], ...] }, sets become { "[[Set]]": [...] }, and long arrays and objects are cut short.
Errors
Section titled “Errors”| Field | Type | Notes |
|---|---|---|
at | number | |
message | string | |
source | string or null | The script URL |
line | number or null | |
column | number or null | |
stack | string or null |
Stacks are as the browser reported them, usually pointing at minified code.
NetworkEntry
Section titled “NetworkEntry”| Field | Type | Notes |
|---|---|---|
id | string | The browser’s request id, or page-<at>-<index> for a request only the page saw, such as one a service worker answered |
at | number | When the request started |
url | string | |
method | string | |
resourceType | string | The Chrome resource type, such as xmlhttprequest, fetch or other |
requestHeaders | object[] | { name, value } pairs |
responseHeaders | object[] | { name, value } pairs. Empty for requests only the page saw |
requestBody | string or null | Only for fetch and XMLHttpRequest calls |
responseBody | string or null | Only for fetch and XMLHttpRequest calls with a text response |
outcome | object | See below |
Bodies are text, cut at 256 KB. A body that is not text is replaced by a description in square brackets, such as [binary 512 bytes], [image/png] or [3145728 bytes, not captured]. Form data is written as key=value pairs joined with &, and files in it as [file name.pdf].
outcome has a state field that decides its other fields:
state | Other fields |
|---|---|
completed | status (number), statusText, durationMs, fromCache (boolean) |
failed | error (the browser’s error message), durationMs |
pending | None. The request had not finished when the capture ended |
Faylee counts a request as failed when its state is failed, when it completed with a status of 400 or more, or when it is a GraphQL request whose JSON response has an errors field, since GraphQL servers answer 200 even for errors.
{ "id": "18234.77", "at": 4950, "url": "https://shop.example.com/api/orders", "method": "POST", "resourceType": "fetch", "requestHeaders": [{ "name": "content-type", "value": "application/json" }, { "name": "authorization", "value": "[redacted]" }], "responseHeaders": [{ "name": "content-type", "value": "application/json" }], "requestBody": "{\"cart\":\"c_91\",\"card\":\"saved\"}", "responseBody": "{\"error\":\"payment_service_unavailable\"}", "outcome": { "state": "completed", "status": 500, "statusText": "Internal Server Error", "durationMs": 812, "fromCache": false }}SocketConnection
Section titled “SocketConnection”One WebSocket or EventSource connection and what crossed it while recording.
| Field | Type | Notes |
|---|---|---|
id | string | Stable for the connection within a page |
transport | string | websocket or eventsource |
url | string | |
openedAt | number | 0 when it was already open when the recording started |
closedAt | number or null | |
closeCode | number or null | |
closeReason | string or null | |
failed | boolean | The connection reported an error |
messages | object[] | { at, direction, data, size }. direction is sent or received |
droppedMessages | number | How many older messages were left out |
A connection keeps its newest 1,000 messages. Text messages are cut at 4 KB, and size is the full length. Binary messages are described as [binary <n> bytes].
UserAction
Section titled “UserAction”Each entry in actions is { at, action }. action has a type field that decides its other fields:
type | Fields |
|---|---|
click | target, x, y |
input | target, value |
select | target, value |
submit | target |
key | target, key, such as Enter or Escape |
scroll | x, y |
resize | width, height |
navigation | url, cause, and optionally title |
cause is load (a full page load), history (a single-page app route change), hash (a fragment change) or tab (the user switched tabs). title is the document title once the new page settled.
target describes the element:
| Field | Type | Notes |
|---|---|---|
selector | string | A CSS selector for the element |
tag | string | Lowercase tag name |
role | string or null | Its ARIA role |
name | string or null | Its accessible name |
testId | string or null | Its test id attribute |
role, name and testId are enough to build a Playwright or Testing Library locator. Prefer them over selector when they are present.
{ "at": 4900, "action": { "type": "click", "target": { "selector": "form#pay > button", "tag": "button", "role": "button", "name": "Pay now", "testId": null }, "x": 812, "y": 604 } }Annotation
Section titled “Annotation”A mark the reporter drew over the page while recording, or over a screenshot before sharing it. The video or screenshot shows it as pixels. The annotation is the same mark as data.
| Field | Type | Notes |
|---|---|---|
id | string | Stable within the capture. A mark keeps its id when it is moved |
at | number | When it was first drawn. A screenshot’s marks are all at 0 |
until | number or null | When it left the page (undone, cleared or faded), or null if it was still there at the end |
tool | string | pen, box, circle, arrow, line, text or blur |
color | string or null | Hex. null for blur |
from, to | { x, y } or null | Where the drag started and ended. A pen’s first and last point. A text note’s top left is from, with no to |
points | { x, y }[] or null | The whole pen stroke. null for other tools |
box | { x, y, width, height } | The area the mark covers |
text | string or null | A text note’s words |
target | object or null | The element under the mark’s anchor, in the same shape as an action’s target. The anchor is an arrow’s head, a note’s start, and the middle of anything else |
redacted | boolean | true for blur |
Positions are in viewport CSS pixels from the top left of the page as it was on screen, so read them against environment.viewport. A full-page screenshot’s marks can fall above or below the viewport.
A blur pixelates what is under it, so it hides rather than points. It keeps only its box, with no color, text or target, because naming the element under it could give away what it hides.
Changes the reporter made to the page while recording, to show how it should look or read.
| Field | Type | Notes |
|---|---|---|
at | number | |
type | string | style or text |
selector | string | The changed element |
property | string or null | The CSS property for a style change, null for text |
before | string | What the page had |
after | string | What the reporter set |
Environment
Section titled “Environment”| Field | Type | Notes |
|---|---|---|
userAgent | string | |
browser | { name, version } | |
os | { name, version } | |
language | string | |
languages | string[] | |
timezone | string | An IANA name, such as Europe/Berlin |
viewport | { width, height } | CSS pixels |
screen | { width, height, pixelRatio } | |
colorScheme | string | light or dark |
cpuCores | number or null | |
memoryGb | number or null | |
network | object or null | { effectiveType, downlinkMbps, rttMs } |
online | boolean | |
cookiesEnabled | boolean | |
battery | object, null or absent | { level, charging }, level from 0 to 1 |
incognito | boolean, null or absent | |
deviceModel | string, null or absent |
battery, incognito and deviceModel were added later, so older bundles lack them. The environment is the one of the last page load before the capture ended.
Vitals
Section titled “Vitals”| Field | Type | Notes |
|---|---|---|
at | number | |
metric | string | lcp, fcp, ttfb, cls, inp, long-task or memory |
value | number | Milliseconds for times, unitless for cls, megabytes for memory |
Custom metadata
Section titled “Custom metadata”What the page passed to window.faylee.metadata(), such as a user id or a plan.
| Field | Type | Notes |
|---|---|---|
at | number | |
data | object | The values passed, as JSON |
A page can call it more than once. To get the current values, merge the entries in order, later values winning.
Linked sessions
Section titled “Linked sessions”The same visit as recorded by a session replay or monitoring tool on the page.
| Field | Type | Notes |
|---|---|---|
provider | string | logrocket, fullstory, sentry, posthog or datadog |
url | string or null | A link to the session in that tool |
sessionId | string or null | The tool’s session id. A tool gives one or both |
DOM events
Section titled “DOM events”dom holds rrweb events, kept as rrweb wrote them, for replaying the page. Each has a numeric type and a numeric timestamp. Pass the list to an rrweb player to rebuild the page.
Their timestamp is not an offset. It is Date.parse(startedAt) + at: an epoch-like number on the recording’s clock. Subtract Date.parse(startedAt) to compare it with other entries.
The list starts with the last full snapshot at or before the start of the timeline, so the player has a complete page to apply later changes to.
Masked fields have their values replaced in the DOM events. See Redaction.
Redaction
Section titled “Redaction”The Faylee extension hides secrets before a bundle is uploaded. Expect these markers in place of the original values:
| Marker | Where |
|---|---|
[redacted] | A hidden header value, query parameter, JSON value, form field or text match |
Bearer [redacted] | A credential found in text. Basic and Token credentials keep their scheme the same way |
//[redacted]@ | A user:password@ in a URL |
•••• | The value of an input action in a sensitive or masked field |
What is hidden:
- Headers:
authorization,proxy-authorization,cookie,set-cookie,x-api-key,x-auth-token,x-csrf-token,x-xsrf-token,x-amz-security-token, plus any added in the extension’s redaction settings. - Keys in query strings, JSON bodies, form bodies, console arguments and metadata whose names look secret, such as
password,secret,apiKey,token,session,cvvoriban. - Text that looks like a JWT, a
Bearer,BasicorTokencredential, or a password in a URL. - Fields that are password inputs, fields whose
autocompleteasks for card details, a one-time code or a password, and anything inside an element with thedata-faylee-maskattribute or thefaylee-maskclass. Their values are masked in bothactionsanddom.
Page titles, stacks, environment and vitals are not redacted. The reporter can also remove whole sections or requests before uploading, so a list can be shorter than what happened on the page.
Versioning
Section titled “Versioning”version is the number 2. Faylee adds fields to version 2 without changing the number, always as lists that may be missing or as fields that may be absent or null. When you parse a bundle:
- Ignore keys you do not know.
- Treat a missing list as empty.
- Treat a missing field inside an object as
null.
The version changes only when an existing field changes meaning or shape. Recordings made with the older version 1 format have "version": 1 and a different shape. Faylee’s viewer upgrades them as it reads them, but the capture endpoint returns the file as stored. If you read old recordings, check version first and skip bundles that are not 2.