-
-
Notifications
You must be signed in to change notification settings - Fork 0
Tap Scope and Muting
This page goes under the hood of the tap: exactly how scope is decided, how the hooks capture, and the full semantics of muting. Read this when you're designing a tap for a real deployment rather than a demo.
For every message emission in the runtime, the tap asks: is the emitting node in scope? The answer is computed in a strict order, first match wins:
- Flight-recorder family? → out, unconditionally. All four node types are hard-excluded — even under All flows, even if explicitly named in the Nodes field. Recorders never record recorders; this is what makes feedback loops (a tap capturing its own incident emissions, which appear in the next dump, which…) impossible by construction rather than by configuration.
- In the exclude-nodes or exclude-types list? → out. Exclusions always beat selectors.
- In the Nodes list? → in.
- In the Types list? → in.
- All flows checked? → in.
- The node's flow (its tab) in the Flows list? → in.
- Otherwise → out.
So the algebra is (flows ∪ nodes ∪ types) − exclusions, with the family exclusion sitting above everything.
That evaluation runs on every message send in the runtime, matched or not, so it must be cheap: the verdict for each source node id is computed once and cached. The cache is populated lazily — the first time a node is seen sending — which has a useful consequence: a node added to a watched flow by a partial deploy is picked up on first sight, with no tap redeploy needed. The scope is self-healing for additions.
The one edge this creates: a node moved between flows keeps its cached verdict until the tap itself redeploys (a full deploy, or any deploy touching the tap). If you reorganize tabs under a flow-scoped tap, redeploy the tap. Node-id and type scoping are unaffected — those verdicts don't depend on which flow the node lives in.
Nodes inside a subflow instance report their real types (function, mqtt in, …) but belong to the subflow's internal flow, not the tab you placed the instance on — so flow-scoping a tab does not reach inside subflow instances on it. The instance node itself has type subflow:<subflow-template-id>. Practical guidance: to record a subflow's internals, scope by the node types you care about, or accept that subflow internals are out of a flow-scoped tap's reach.
The tap registers with Node-RED's messaging hooks (available since Node-RED 1.1):
-
onSendfires per message emission, before routing. The tap receives the emitting node (id, name, type, and which output port the message left on — all preserved into the record'ssource) and the message itself. A message fanning out to several wires arrives as several send events in one batch; the tap deduplicates by_msgidwithin the batch, so a fan-out is one record, not five. -
onComplete(registered only when auto-error dump is enabled) fires when a node finishes handling a message or logs an error against it. On an error from an in-scope node, the tap callsdump("error", …)with the error, the erroring node, and the message it was handling as context.
Capture then flows through exactly the same engine as the inline recorder: clone → redact → encode → size-check, with the same guarantees. A capture failure of any kind is surfaced as a recorderError event and never disturbs the runtime — the tap is an observer, never a hazard.
Hooks see messages between nodes. The tap cannot see:
- inside a function node's logic (only what it sends);
- messages a node swallows without sending — though the swallow shows up honestly as a gap in that source's cadence, which the report's
⚠anomaly flag will often catch; - errors from nodes that don't report properly — auto-error dump depends on
node.error(err, msg), which well-behaved nodes use and some older contrib nodes don't; - a hard runtime crash — no hook fires when the process dies. That case is exactly what persistence exists for: the debounced write-behind means the recording on disk is at most a few seconds behind reality, and it survives to be read after the restart.
Scope membership is static configuration: the set of things that could appear in a recording never changes at runtime, which keeps recordings interpretable. Muting is the operational knob layered on top: silence one channel while the tape rolls.
Targets. A mute names a node ({"node":"<id>"}), a type ({"type":"mqtt in"}), or a flow ({"flow":"<tab id>"}). A message is suppressed if its source matches any active mute.
Nothing invisible. Three mechanisms guarantee a muted gap is never mistaken for real silence:
- Every suppressed message increments a per-target counter, reported in
queryresults. - Muting and unmuting each lay an annotation mark on the tape, in sequence position — the unmute mark carries the suppressed count. An incident spanning a mute window shows exactly where the gap begins, ends, and how much it hid. Reports render these as
── MUTED ──/── UNMUTED ──rules. - The
muted/unmutedevents fire on the events output.
Muted failures still dump. Auto-error dump deliberately ignores mutes: you muted a source's chatter, not its failures. A flapping sensor can be silenced without losing the incident when it genuinely breaks.
Ephemeral by design. Mutes reset on deploy and restart. A mute silently outliving a restart is a trap ("why has nothing from that sensor been recorded for six weeks?"); the marks on the tape preserve the history of past mutes even though the mute state itself doesn't persist. Permanent silences belong in the exclusion config — the division is: configuration is permanent, commands are operational.
Validation. A mute targeting a node the tap has already seen and rejected as out-of-scope is refused with a muteIgnored event — you probably typo'd the id. Targets the tap can't verify (never-seen nodes, types, flows) are accepted, since they may become relevant.
The debug tab. A dedicated tab holding one tap (scoped to your production flows), a report node with file output, and a control panel. Entirely non-invasive — deployable and removable without touching the flows being observed.
The I/O boundary recorder. Scope by types — mqtt in, mqtt out, http request, websocket in — to record every message crossing your system's external boundary, regardless of which flow it lives in. Add exclusions for the chatty ones.
Excluding debug. If your flows use debug nodes liberally, add debug to the exclude-types list (the shipped examples do) — otherwise every sidebar delivery is also on your tape.
Flight Recorder
Node Reference
Reference
Guides
Testing