Problem
Several production code paths still call console.warn / console.log directly. In a Vercel Functions deployment these end up in the runtime log stream, but as unstructured strings — not joinable with sync run ids, not filterable by source type, and easy to lose in volume. We should standardize on a small structured-logger helper before more code copies the pattern.
Evidence
Production-path console calls:
src/lib/sync/sources/anthropic-workspace.ts:237,258 — console.warn on per-workspace cost-sync edge cases.
src/lib/anthropic-keys.ts:49 — console.warn for missing keys.
src/lib/db/seed.ts:34 — console.log printing the seeded admin credentials to stdout (only runs locally on pnpm db:seed, but worth tightening — see acceptance criteria).
src/actions/copilot.ts:72, src/app/api/invoices/ingest/route.ts:297, src/app/api/profile/route.ts:97 — console.error on failure paths. These are acceptable as fallbacks but should still get the structured wrapper.
Acceptable / out of scope:
src/lib/db/baseline-migration.ts:47,65,66 — dev migration helper, runs once.
- Test files and scripts under
scripts/.
Proposed approach
- Create
src/lib/logger.ts with a tiny structured-logger wrapper. Keep it dependency-free (no Pino, no Winston) — just:
type Level = \"debug\" | \"info\" | \"warn\" | \"error\";
export function log(level: Level, message: string, fields?: Record<string, unknown>) {
const payload = { level, message, ts: new Date().toISOString(), ...fields };
// Vercel parses JSON in console output as structured logs
(level === \"error\" ? console.error : console.log)(JSON.stringify(payload));
}
- Replace each
console.warn / console.log listed above with log(\"warn\", \"...\", { sourceType, runId, userId, … }).
- Replace existing
console.error calls in the listed action/route files with log(\"error\", \"...\", { error: serializeError(err) }).
- For
src/lib/db/seed.ts:34: stop logging the seeded password. Print only the email and a note that the password was set from the env var or written to the screen exactly once on first run, then never again.
- Add a Vitest unit test that captures
console.log and asserts JSON shape.
- Document the helper in
CLAUDE.md under "Code Style" as the canonical way to log.
Acceptance criteria
Verification
- Run a sync that triggers the warn paths (e.g. anthropic-workspace sync against a workspace with no costs) and grep the output for
\"level\":\"warn\".
- Run
pnpm db:seed → stdout shows the email but not the password.
- Search the repo for
console\\.(log|warn|error) and confirm no new matches outside src/lib/logger.ts, tests, and explicitly justified exceptions.
Problem
Several production code paths still call
console.warn/console.logdirectly. In a Vercel Functions deployment these end up in the runtime log stream, but as unstructured strings — not joinable with sync run ids, not filterable by source type, and easy to lose in volume. We should standardize on a small structured-logger helper before more code copies the pattern.Evidence
Production-path console calls:
src/lib/sync/sources/anthropic-workspace.ts:237,258—console.warnon per-workspace cost-sync edge cases.src/lib/anthropic-keys.ts:49—console.warnfor missing keys.src/lib/db/seed.ts:34—console.logprinting the seeded admin credentials to stdout (only runs locally onpnpm db:seed, but worth tightening — see acceptance criteria).src/actions/copilot.ts:72,src/app/api/invoices/ingest/route.ts:297,src/app/api/profile/route.ts:97—console.erroron failure paths. These are acceptable as fallbacks but should still get the structured wrapper.Acceptable / out of scope:
src/lib/db/baseline-migration.ts:47,65,66— dev migration helper, runs once.scripts/.Proposed approach
src/lib/logger.tswith a tiny structured-logger wrapper. Keep it dependency-free (no Pino, no Winston) — just:console.warn/console.loglisted above withlog(\"warn\", \"...\", { sourceType, runId, userId, … }).console.errorcalls in the listed action/route files withlog(\"error\", \"...\", { error: serializeError(err) }).src/lib/db/seed.ts:34: stop logging the seeded password. Print only the email and a note that the password was set from the env var or written to the screen exactly once on first run, then never again.console.logand asserts JSON shape.CLAUDE.mdunder "Code Style" as the canonical way to log.Acceptance criteria
src/lib/logger.tsexists withlog(level, message, fields?).console.warn/console.log/console.errorsites in production paths use the helper.src/lib/db/seed.tsno longer prints the seeded password to stdout.pnpm lint && pnpm typecheck && pnpm testpass.Verification
\"level\":\"warn\".pnpm db:seed→ stdout shows the email but not the password.console\\.(log|warn|error)and confirm no new matches outsidesrc/lib/logger.ts, tests, and explicitly justified exceptions.