Browser Logs
Front-end errors do not stay in the browser. Caspian's dev stack forwards every PulsePoint failure to your terminal and to a file an AI agent can read, so a broken route stops being something you only find by opening DevTools.
Why this exists
PulsePoint reports runtime problems through
console.error with a
[PP-ERROR] prefix. Those never reached
the npm run dev terminal, so a broken
route looked identical to a working one. The worst case is a template
typo like an unquoted
class={...}: the component root never
compiles, the page renders blank, and
nothing is printed anywhere.
An AI agent had it worse still. The developer usually owns the
npm run dev terminal, so its stdout is
invisible to an agent session — and starting a second dev server
to get its own copy would bind different ports and orphan the browser
tab the developer is actually looking at. So every event is also
appended to a file any process can read.
How It Works
Browser
A small client script hooks
console.error/warn,
window.onerror, and unhandled promise
rejections, then POSTs them.
BrowserSync middleware
BrowserSync already proxies every request, so the receiver costs no extra port. It prints to the terminal and appends to the log file.
You, or an agent
Read it in the terminal as it happens, or run
npm run logs for a per-route digest
derived from the file.
Two design choices are worth knowing, because they explain behavior you will otherwise find surprising:
-
The tag comes from the render pipeline, not the proxy.
main.pyinjects<script src="/__pp-devlog.js">duringfinalize_html(...), gated on an environment variable that only the dev stack sets. BrowserSync's own snippet injection does not fire against these proxied responses. -
Transport is plain HTTP, not the BrowserSync socket.
The errors most worth catching fire during
pp.mount()at page load — often before that socket finishes connecting. APOSTworks from the first millisecond of page life.
CASPIAN_BROWSER_SYNC_PORT must be set, which
only happens when the dev stack spawns the server. It cannot reach
production or a static export, and the bridge module is imported solely by
the dev-time BrowserSync config.
[PP-ERROR] /
[PP-WARN] output plus genuine uncaught
errors and unhandled rejections are forwarded. Your ordinary
console.log debugging stays in the browser
where it belongs, so the terminal keeps signal high. Identical messages are
de-duplicated within a one-second window.
Reading The Digest
npm run logs derives current per-route
status from the log. npm run check prints
the same digest at the end of its run, but never lets it change the exit
code — whether a route has been exercised depends on someone
clicking around, and a gate that flaky gets ignored.
Browser log (.casp\browser-log.jsonl) ============================================================ LIVE dev session active (pid 24036, port 5090, started 19:09:02) 1 ERROR(S) /dashboard last load 19:24:32 46s ago [PP-ERROR] Template Expression Failed: "total" total is not defined at eval (eval at compile (.../pp-reactive-v2.min.js:1:173249)) CLEAN / last load 19:23:21 1m ago CLEAN /docs last load 19:24:05 1m ago 1 route(s) failing in the browser. Routes not listed were never opened -- that is no signal, not a pass.
Status vocabulary
The hard part is not reporting errors, it is not lying about their absence. Each status answers a different way a naive error log misleads you.
| Status | Meaning |
|---|---|
| CLEAN | That route was opened and rendered without error. A route absent from the listing was never opened — that is no signal, not a pass. |
| N ERROR(S) |
Errors fired during the route's most recent load. One clean
reload retires every earlier error for that route, reported as
N earlier error(s) resolved.
|
| NEEDS RECHECK | The status that matters most. A reload re-runs mount, so it genuinely re-tests a mount-phase error — but it never clicks a button. An error thrown from an event handler is carried here instead of being cleared. Repeat the interaction, then re-run. |
| UNCONFIRMED | An error with no matching load in this log — almost always a tab left open across a dev restart, reporting against a page rendered by the previous session. Reload that route before treating it as a live bug. |
| NONE WARN |
NONE means no dev session ever
wrote — nothing has been observed.
WARN means the log is left over
from an exited session, so every line is history.
|
The Log File
Events are appended to
.casp/browser-log.jsonl
— one JSON event per line, gitignored, and truncated per dev session
because .casp/ is recreated at startup. It
is the file that makes this readable by a process that does not own the
terminal.
Do not diagnose from the raw JSONL.
It is history, not state. It can hold errors that were resolved
minutes ago, and errors carried across a source change. Always prefer
npm run logs, which derives current
status. If you do read the file directly, the
session line carries a
readme stating the rule: a later
load or
resolved supersedes earlier errors on
that route, except
phase: "interaction" errors, which a
reload cannot re-test.
Two properties that keep it honest
-
Successful loads are recorded, not just errors.
A clean reload writes nothing on its own, so without
loadevents a fixed error would sit in the file forever and you would “fix” a bug that no longer exists. Errors are tied to their load by a client-generated page id, never by arrival order, because two reports can land out of sequence. -
It is compacted on every source change, not appended forever.
When your source changes, the file is rewritten down to the session
header, a restart marker, and the errors still open; resolved history
is dropped. Survivors are marked
carriedand dropped at the next compaction, so a stale interaction error cannot haunt the log. Reading the digest costs about the same regardless of how long the session has been running, because it is bounded by route count, not time.
Acting On A Failure
The digest gives you the route, the message, and the top stack frames. Fix
the owning src/app/<route>/index.py or
imported Python component, then get
the route exercised again and re-run. The flip to
CLEAN is the confirmation.
A mount error needs a reload
It fired within two seconds of the page load, so re-running mount is a real re-test.
An interaction error needs the same click
A reload will not clear it and should not be expected to. Repeat the click or submit that triggered it.
npm run dev just to get a log.
That command begins by deleting .casp/ and
caches/ out from under the running server,
then binds different ports and rewrites the BrowserSync config —
orphaning the browser tab you were looking at. Use
npm run logs against the session that is
already running.
Commands
npm run logs
Human-readable per-route digest.
npm run logs -- --json
Machine-readable status, for scripts and agents.
npm run logs -- --fail-on-error
Exit non-zero if any route is dirty. Opt-in.
npm run check -- --no-browser
Run the quality gate without the browser-log section.
settings/dev-log-bridge.ts,
settings/browser_log.py, and the injection
hook in main.py — rather than code
inside the installed casp package, so you can
read and adapt all of it. Known limit: the client script runs per full
document load, so SPA navigations do not emit a load event.
The log tracks full page loads only.