Skip to content

Data Formats

mchristegh edited this page Jul 14, 2026 · 1 revision

The JSON reference for anyone consuming recorder output in their own tools. These shapes are the package's public contracts: fields may be added in future versions, but existing fields won't change meaning.

The envelope

Every message on a recorder's incident, query, and events outputs:

{
  "topic": "<node name or node type>",
  "recorderEvent": "incident",
  "recorderState": "recording",
  "storeId": "<recorder node id>",
  "payload": { ...event-specific... }
}

recorderEvent values: incident, incidentSuppressed, marked, wrapped, truncatedMessage, paused, resumed, cleared, persisted, recorderError, commandIgnored, query — plus, on taps, muted, unmuted, muteIgnored.

Incident

{
  "incidentId": "a1b2c3d4-…",
  "storeId": "abc123",
  "trigger": "error",
  "triggeredAt": 1783987005544,
  "context": { },
  "meta": {
    "package": "node-red-contrib-flight-recorder@0.2.0",
    "nodeRed": "4.0.2",
    "node": "v22.11.0",
    "hostname": "yellow"
  },
  "stats": { …see Stats… },
  "records": [ …see Record… ]
}

trigger is manual, error, or watermark. context depends on the trigger: manual dumps carry whatever msg.context the command supplied (or null); error dumps carry { error, source, msg } with the error structured (see encodings) and the emitting node identified; watermark dumps carry { condition, matchedSeq }. A context whose encoding exceeds the record byte cap is replaced by { "__contextTruncated": true, "preview": "…" }. meta (added in 0.2.0) records what produced the incident; older incidents simply lack it.

Record

{
  "seq": 1042,
  "capturedAt": 1783987005544,
  "deltaMs": 2001,
  "source": { "id": "f1", "name": "worker", "type": "function", "port": 0 },
  "truncated": false,
  "redacted": ["payload.password"],
  "annotation": null,
  "msg": { }
}
  • seq is monotonic forever — it survives buffer wraps, clear, and restarts, so records are globally orderable.
  • deltaMs is the time since the previous accepted capture, computed at capture time; null on the first record.
  • source is the recorder itself for inline captures, the true emitting node (with output port) for tap captures. Annotation marks have a null source.
  • truncated: true means the message exceeded the byte cap and msg is in summary form (below); redacted lists the property paths that were masked.
  • Annotation marks: annotation is set (the mark payload, or {muted: …} / {unmuted: …, suppressedCount} for mute boundaries) and msg is null. Marks occupy sequence positions on the tape.
  • Records restored from a persist file after a restart additionally carry "restored": true.

Summary form (captureMode: "summary" or truncation fallback)

{
  "payloadType": "object",
  "payloadBytes": 1049311,
  "payloadPreview": "{\"blob\":\"zzzz…",
  "topic": "sensors/demo",
  "msgKeys": ["payload", "topic", "_msgid"]
}

payloadBytes reflects the original size — a truncated record still tells you it was a megabyte.

Value encodings inside msg

Original Stored as
Circular reference "[Circular]"
Buffer / TypedArray { "__type": "buffer", "length": n, "preview": "<hex of first 32 bytes>" }
Error { "__type": "error", "name", "message", "stack" } (stack capped)
Function "[Function]"
Date ISO string
HTTP msg.req / msg.res "[HttpRequest]" / "[HttpResponse]" — never deep-encoded
undefined object properties dropped (JSON semantics)

Redaction ("[REDACTED]") is applied before encoding or previewing.

Stats (shared by incidents and query snapshots)

{
  "recordCount": 47,
  "firstSeq": 955, "lastSeq": 1042,
  "spanMs": 94100,
  "droppedWhilePaused": 0,
  "wrapCount": 12,
  "truncatedCount": 1
}

spanMs is first-capture → last-capture; null when empty, 0 for a single record.

Query snapshot

{
  "storeId": "abc123",
  "state": "recording",
  "capacity": 100,
  "restoredFromPersist": false,
  "config": { "capacity", "captureMode", "maxRecordBytes", "persist", "incidentCooldownMs" },
  "stats": { },
  "tap": {
    "autoErrorDump": true,
    "mutes": [ { "kind": "node", "value": "m1", "suppressedCount": 4 } ],
    "scopeSeen": { "inScope": 3, "outOfScope": 6 }
  }
}

The tap block appears only on tap recorders. scopeSeen counts distinct source nodes evaluated so far.

Control node receipt

{ "ok": true, "command": "dump", "targetId": "abc123", "result": { }, "reason": null }

with msg.inReplyTo echoing the triggering _msgid. On failure: ok: false, result: null, and a reason string.

Report node output (msg.report)

{
  "kind": "incident",
  "format": "html",
  "source": { …the original payload, untouched… },
  "files": [ { "kind": "report", "path": "", "bytes": 8412, "ok": true } ]
}

kind is incident, snapshot, event, or unknown; files entries are kind: "report" | "raw" with per-file ok/reason. Raw JSON files are the incident/snapshot serialized with JSON.stringify(payload, null, 2) — lossless.

Persist file (<userDir>/flight-recorder/<nodeId>.json)

{
  "formatVersion": 1,
  "storeId": "abc123",
  "savedAt": 1783987005544,
  "seqCounter": 1042,
  "state": "recording",
  "stats": { },
  "records": [ ]
}

The format is versioned from day one; a file with an unrecognized formatVersion (or any corruption) logs a warning and the recorder starts fresh — it never blocks startup. Files are written atomically (temp + rename), so an existing file can't be corrupted by a crash mid-write.

Clone this wiki locally