Skip to content

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.

FieldTypeNotes
version2Always the number 2 for this format
kindstringvideo, replay or screenshot
startedAtstringISO 8601. Wall-clock start of the timeline
durationMsnumberLength of the timeline in milliseconds, pauses excluded. 0 or more
environmentobject or nullThe browser and device. See Environment
pagesobject[]Pages open during the capture. See Pages
consoleobject[]Console messages. See Console
errorsobject[]Uncaught errors and unhandled rejections. See Errors
networkobject[]Requests, sorted by at. See NetworkEntry
actionsobject[]What the user did. See UserAction
domobject[]rrweb events for the page replay. See DOM events
vitalsobject[]Web Vitals and runtime measurements. See Vitals
customobject[]Metadata the page attached. See Custom metadata
socketsobject[]WebSocket and EventSource connections. See SocketConnection
linkedSessionsobject[]The same visit in other tools. See Linked sessions
editsobject[]Changes the reporter made to the page. See Edits
annotationsobject[]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.

FieldTypeNotes
atnumberWhen the page loaded. The page open when recording started is at 0
urlstring
titlestring
FieldTypeNotes
atnumber
levelstringlog, info, warn, error, debug or trace
argsarrayThe arguments passed to the console call, as JSON values
stackstring 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.

FieldTypeNotes
atnumber
messagestring
sourcestring or nullThe script URL
linenumber or null
columnnumber or null
stackstring or null

Stacks are as the browser reported them, usually pointing at minified code.

FieldTypeNotes
idstringThe browser’s request id, or page-<at>-<index> for a request only the page saw, such as one a service worker answered
atnumberWhen the request started
urlstring
methodstring
resourceTypestringThe Chrome resource type, such as xmlhttprequest, fetch or other
requestHeadersobject[]{ name, value } pairs
responseHeadersobject[]{ name, value } pairs. Empty for requests only the page saw
requestBodystring or nullOnly for fetch and XMLHttpRequest calls
responseBodystring or nullOnly for fetch and XMLHttpRequest calls with a text response
outcomeobjectSee 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:

stateOther fields
completedstatus (number), statusText, durationMs, fromCache (boolean)
failederror (the browser’s error message), durationMs
pendingNone. 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 }
}

One WebSocket or EventSource connection and what crossed it while recording.

FieldTypeNotes
idstringStable for the connection within a page
transportstringwebsocket or eventsource
urlstring
openedAtnumber0 when it was already open when the recording started
closedAtnumber or null
closeCodenumber or null
closeReasonstring or null
failedbooleanThe connection reported an error
messagesobject[]{ at, direction, data, size }. direction is sent or received
droppedMessagesnumberHow 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].

Each entry in actions is { at, action }. action has a type field that decides its other fields:

typeFields
clicktarget, x, y
inputtarget, value
selecttarget, value
submittarget
keytarget, key, such as Enter or Escape
scrollx, y
resizewidth, height
navigationurl, 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:

FieldTypeNotes
selectorstringA CSS selector for the element
tagstringLowercase tag name
rolestring or nullIts ARIA role
namestring or nullIts accessible name
testIdstring or nullIts 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 } }

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.

FieldTypeNotes
idstringStable within the capture. A mark keeps its id when it is moved
atnumberWhen it was first drawn. A screenshot’s marks are all at 0
untilnumber or nullWhen it left the page (undone, cleared or faded), or null if it was still there at the end
toolstringpen, box, circle, arrow, line, text or blur
colorstring or nullHex. null for blur
from, to{ x, y } or nullWhere 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 nullThe whole pen stroke. null for other tools
box{ x, y, width, height }The area the mark covers
textstring or nullA text note’s words
targetobject or nullThe 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
redactedbooleantrue 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.

FieldTypeNotes
atnumber
typestringstyle or text
selectorstringThe changed element
propertystring or nullThe CSS property for a style change, null for text
beforestringWhat the page had
afterstringWhat the reporter set
FieldTypeNotes
userAgentstring
browser{ name, version }
os{ name, version }
languagestring
languagesstring[]
timezonestringAn IANA name, such as Europe/Berlin
viewport{ width, height }CSS pixels
screen{ width, height, pixelRatio }
colorSchemestringlight or dark
cpuCoresnumber or null
memoryGbnumber or null
networkobject or null{ effectiveType, downlinkMbps, rttMs }
onlineboolean
cookiesEnabledboolean
batteryobject, null or absent{ level, charging }, level from 0 to 1
incognitoboolean, null or absent
deviceModelstring, 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.

FieldTypeNotes
atnumber
metricstringlcp, fcp, ttfb, cls, inp, long-task or memory
valuenumberMilliseconds for times, unitless for cls, megabytes for memory

What the page passed to window.faylee.metadata(), such as a user id or a plan.

FieldTypeNotes
atnumber
dataobjectThe 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.

The same visit as recorded by a session replay or monitoring tool on the page.

FieldTypeNotes
providerstringlogrocket, fullstory, sentry, posthog or datadog
urlstring or nullA link to the session in that tool
sessionIdstring or nullThe tool’s session id. A tool gives one or both

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.

The Faylee extension hides secrets before a bundle is uploaded. Expect these markers in place of the original values:

MarkerWhere
[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, cvv or iban.
  • Text that looks like a JWT, a Bearer, Basic or Token credential, or a password in a URL.
  • Fields that are password inputs, fields whose autocomplete asks for card details, a one-time code or a password, and anything inside an element with the data-faylee-mask attribute or the faylee-mask class. Their values are masked in both actions and dom.

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.

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.