Logging#
Log messages reach the host (Eyra mono’s /api/feldspar/log) via two
separate paths. Understanding which path a message takes, and why, is
important for knowing what the host will see and what PII rules apply.
Two paths to the host#
sequenceDiagram
participant FB as FlowBuilder<br/>flow_builder.py
participant SW as ScriptWrapper<br/>main.py
participant WB as Worker boundary<br/>py_worker.js
participant WPE as WorkerProcessingEngine<br/>worker_engine.ts
participant LF as LogForwarder<br/>logging.ts
participant CR as CommandRouter<br/>command_router.ts
participant LB as LiveBridge<br/>live_bridge.ts
participant HOST as Eyra mono
note over FB,SW: Path A — Python emit_log
FB->>SW: yield CommandSystemLog(level, message)
SW->>WB: postMessage runCycleDone
WB->>WPE: handleEvent('runCycleDone')
WPE->>CR: handleRunCycle → onCommand(CommandSystemLog)
CR->>LB: bridge.send(command)
LB->>HOST: port.postMessage(CommandSystemLog)
WPE->>LF: logger.flush()
note over WPE,LF: Path B — JS LogForwarder
WPE->>LF: worker.onerror → logger.log('error', ...)
LF->>LB: flush → sendLogs(entries)
LB->>HOST: port.postMessage(CommandSystemLog ×N)
Path A: Python emit_log#
Origin: Python code explicitly calls yield from ph.emit_log(level, message).
Route:
ph.emit_log()yields aCommandSystemLog(level, message)ScriptWrapper.send()serialises it via.toDict()and returns itpy_worker.jspostsrunCycleDonewith the command dictWorkerProcessingEngine.handleRunCycle()passes it toCommandRouterCommandRouter.onCommandSystem()callsbridge.send(command)LiveBridge.send()callsport.postMessage(command)— the message arrives at Eyra mono
PII rule: All messages on Path A must be PII-free. This means:
Error counts from
ExtractionResult.errors(type names and counts only)Flow milestone strings (e.g.
[LinkedIn] Consent: accepted)File size in bytes
No exception messages, no file paths, no participant data
Where to use it: In FlowBuilder.start_flow() and script.py. Already
called for you at every standard milestone — you only need to add custom
emit_log calls if you add non-standard flow steps.
Path B: JS LogForwarder#
Origin: JavaScript-side errors and infrastructure events.
Sources that feed into LogForwarder:
Source |
Scope |
What it captures |
|---|---|---|
|
Main thread ( |
Synchronous JS errors on the main thread |
|
Main thread ( |
Unhandled promise rejections on the main thread only — not in the worker |
|
Main thread (listening on worker) |
Synchronous worker errors that propagate to the main thread |
|
Main thread |
Worker lifecycle events at |
Scope gap: Unhandled promise rejections inside the worker (e.g. an
unwrap() failure in py_worker.js) are not captured by any of these
sources. WindowLogSource listens on window, not the worker’s global scope.
These rejections are silently lost — visible only in the worker’s DevTools
console.
Route:
Error is captured by
WindowLogSourceorWorkerProcessingEngineLogForwarder.log(level, message, context)adds it to an in-memory bufferError-level entries trigger an immediate
flush(); all other levels flush after each run cycleThe flush callback (wired in
Assembly) callsbridge.sendLogs(entries)LiveBridge.sendLogs()formats each entry as aCommandSystemLog-shapedpostMessageand posts it to the host
PII rule: WindowLogSource captures rich context (memory usage, filename,
line number) into the LogEntry, and LogForwarder records a timestamp.
LiveBridge.sendLogs() drops both context and timestamp — it only
posts level and message to the host (plus a json_string duplicate for
backwards compatibility). The full LogEntry exists in the JS runtime’s
LogForwarder buffer (visible in DevTools) but those extra fields never cross
the bridge. Raw Python tracebacks also never reach Path B — that is handled by
error_flow instead.
Dead handler note: WorkerProcessingEngine.handleEvent() has a case for
eventType: 'error' (a custom worker message), but py_worker.js never posts
this event type. The handler exists but is inert on this branch.
What stays local#
Not everything is forwarded to the host. The standard Python logging module
(via logger = logging.getLogger(__name__)) writes to the Pyodide worker’s
stderr, which appears in the browser’s DevTools console. This is the place
for verbose diagnostic detail — full exception messages, intermediate values,
file contents. It never leaves the participant’s browser.
Where |
What it contains |
Visible to host? |
|---|---|---|
|
PII-free milestones and error counts |
Yes |
|
JS errors, worker crashes, context |
Yes |
|
Full diagnostic detail |
No — browser console only |
CommandSystemLog format#
# Python
CommandSystemLog(level="info", message="[LinkedIn] Consent: accepted").toDict()
# Produces:
{
"__type__": "CommandSystemLog",
"level": "info",
"message": "[LinkedIn] Consent: accepted",
"json_string": '{"level": "info", "message": "[LinkedIn] Consent: accepted"}'
}
json_string is a temporary field for backwards compatibility with older Eyra
mono versions that expect it. It will be removed once the host is updated.
Key files#
File |
Role |
|---|---|
|
|
|
|
|
|
|
Flush trigger, |
|
Wires |
|
|
→ Error handling — what happens when Python throws an unhandled exception