Once your transmitter is wired up (setup.md), the question is what to log and how. Patterns below are starting points — every project layers its own conventions on top.
logger.debug("panel-ctx.joinCleanup.no-joins-path", {
anchorEntityId,
validRelationshipIdsCount: validRelationshipIds.size,
query,
})Two rules:
- The message string is a structured name — kebab + dot, like a routing path:
<area>.<seam>.<event>. Predictable, greppable, stable enough to copy into commit messages. - All data lives in the object, not the string. Strings can't be queried with
->>; objects can. Preferlogger.debug("fetch.start", { url, method })overlogger.debug(\fetch.start url=${url}`)`.
Cheap to write, fast to query later (data->>'url' in reading-logs.md).
Default: every emission lands in .davstack/logs/default.db. If you want a particular debug session, eval run, or repro to live in its own file (archivable-via-mv, isolated views, no cross-session noise), the transmitter just stamps one attribute on each log. The daemon dispatches.
See transmitter-wiring.md for the 3-line sentry.ts snippet. Once routed, session-views.md shows the high-value follow-up: per-DB SQL views tailored to the bug you're hunting.
run_id— already stamped globally by yourSentry.init'sbeforeSendLog(setup.md §2). One per page-load / process.trace_id— propagated automatically byhttpIntegration/browserTracingIntegrationvia Sentry'ssentry-trace+baggageheaders.service— stamped once oninitialScope.tagsat init time.
If you're tempted to thread these manually, the init's misconfigured. Fix the init, not every call site.
When debugging, the receiver gives you a fast loop:
-
State the hypothesis in writing before adding any log.
H3: the joinCleanup effect's stale closure emits a query snapshot from before the click landed.
-
Plant probes at the discriminating boundary — just before + just after the suspected event. Tag each with the hypothesis id (use an array so one probe can carry multiple tags — e.g.
["H3","joinCleanup"]):logger.debug("panel-ctx.joinCleanup.effect-fire", { tags: ["H3"], done: joinCleanupDoneRef.current, relationshipsLoading, anchorEntityId, query, }) logger.debug("panel-ctx.joinCleanup.no-joins-path", { tags: ["H3", "joinCleanup"], query, })
-
Reproduce, then slice:
sqlite3 -header -column .davstack/logs/default.db " SELECT ts, msg, attrs->>'tags' AS tags FROM logs WHERE run_id = '<id>' AND attrs->>'tags' LIKE '%H3%' ORDER BY ts; "
The timeline shows the actual ordering and payloads — not the assumed ones.
-
Strip the probes once the root cause is known. (Or leave them at
debugand archive the session DB out of.davstack/logs/when you're done with it.)
The skill's value is forcing step 1 before step 2 — without a written hypothesis, you tend to dump logs everywhere and re-read noise.
It's fine to log entire state trees, query ASTs, GraphQL responses, etc. Storage is local and cheap; serialization runs in dev only. Don't pre-summarize "just in case."
The trade-off comes at query time, not write time. When you later run:
sqlite3 -header -column .davstack/logs/default.db "
SELECT ts, data->>'next' AS next
FROM logs
WHERE msg LIKE 'panel-ctx.update%'
ORDER BY ts;
"…you'll be staring at a 2KB object per row. A few habits keep this manageable:
- Filter on the indexed
msgcolumn first, then project payload fields with->>. Narrowing on the indexed string first cuts the row set 10–100×. - Prefer
data->>'$.next.entity'over dumping the wholedatablob to terminal. The SQL recipes in reading-logs.md show the shape. - If one specific payload field keeps being the focus of debugging, lift it to a top-level attribute (
tags,hypothesis, etc.) so theWHERE attrs->>'tags' LIKE '%H3%'cut works without a deep walk.
- PII. No scrubbing layer; local dev only.
- Stack traces as separate
erroritems. Sentry'seventenvelope already carries them atexception.values[].stacktrace; emitting a parallellogger.error("…", { stack })produces duplicate rows.
How you scope service, structure the message-string namespaces (<area>.<seam>.<event>), and bucket levels is project-specific. Set them once in your repo's CLAUDE.md / contributing doc — the daemon doesn't care, and tight uniformity matters less than disciplined names + object payloads.