Code logging
Skill jjmendezrodriguez/jm-claude-plugin/skills/code-logging
Production logging best practices for TypeScript applications with pino (server) and PostHog (browser analytics + session replay). Use this when adding logs, choosing log levels, deciding what to record or redact, structuring log payloads, correlating requests, or setting up observability for Edge Functions. Trigger on: "how should I log this?", "console.log is fine, right?", "add observability", any new service / API endpoint / error handler, any debugging session that needs traces. Apply proactively — logs are the production debugger.From its SKILL.md
npx -y skills add jjmendezrodriguez/jm-claude-plugin --skill code-loggingAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 0 stars0 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
SKILL.md
7.0 KB, as published. Nobody here has run it
Logging — TypeScript / pino / PostHog
Staff Engineer standard: logs are your production debugger — structured, contextual, and never leaking secrets.
1. Golden Rule
// ❌ NEVER in production code
console.log("User logged in:", user);
// ✅ ALWAYS use a structured logger
logger.info("user_logged_in", { userId: user.id, method: "email" });
console.log lacks levels, structure, redaction, and shipping. It only works on your laptop.
2. Recommended Stack
| Runtime | Library | Why |
|---|---|---|
| Node / Bun | pino | Fastest structured JSON, low overhead, async transports |
| Browser | PostHog | Analytics + session replay + error tracking in one |
| Edge Functions | console + PostHog | Deno has no pino-pretty; ship via PostHog HTTP API |
| Aggregation | Better Stack / Datadog / Loki | Query, alert, retain |
3. Log Levels
| Level | When | Example |
|---|---|---|
trace | Hyper-detailed, dev only | "entering function parseForm" |
debug | Dev diagnostics | "parsing form data" |
info | Significant business events | "user_signed_up", "payment_processed" |
warn | Attention needed, not broken | "rate_limit_approaching" |
error | Operation failed | "supabase_query_failed" |
fatal | Process may crash / restart | "database_connection_lost" |
Rule: if you wouldn't page someone on it, it's info or lower. If you would, it's error+.
4. What to Log / Never Log
✅ ALWAYS LOG: ❌ NEVER LOG:
- User ID (uuid, not email) - Passwords, tokens, API keys
- Action / event name - Credit card numbers, SSN, PII
- Timestamp (ISO 8601) - Full request bodies w/ secrets
- Request ID (trace correlation) - Session tokens / JWTs
- Error code + message - Raw user input (sanitize first)
- Duration (perf tracking) - Authorization headers
- Feature flag state - Email addresses (use userId)
If unsure, don't log it. PII leaks compound — every log sink amplifies the breach.
5. Logger Setup (pino)
// src/lib/logger.ts
import pino from "pino";
export const logger = pino({
level: process.env.LOG_LEVEL ?? (import.meta.env.DEV ? "debug" : "info"),
// Redact sensitive paths automatically — even if someone passes them by accident
redact: {
paths: [
"password",
"*.password",
"token",
"*.token",
"authorization",
"*.authorization",
"req.headers.cookie",
],
censor: "[REDACTED]",
},
// Pretty in dev, JSON in prod
transport: import.meta.env.DEV
? { target: "pino-pretty", options: { colorize: true } }
: undefined,
// Standard base fields on every log
base: {
service: "auth-api",
env: process.env.NODE_ENV,
},
});
6. Structured Logging Pattern
Convention: event name first as string, context object second. Matches pino's signature and reads naturally.
// ✅ Pattern: logger.<level>(event_name, context)
logger.info("profile_updated", { userId, durationMs: 42 });
logger.error("supabase_query_failed", {
err, // pino auto-serializes Error.stack
userId,
query: "users.select",
retries: 3,
});
// ❌ String concatenation — unsearchable, no fields to filter on
logger.info(`User ${userId} updated profile in ${duration}ms`);
// ❌ Object first then message — works in pino but inconsistent style
logger.info({ userId }, "User logged in");
Why event-name-first:
- Searchable via
event:"profile_updated"filter in any log tool - Stable identifier even when context changes
- Reads like a domain event, not a sentence
7. Request Correlation
Every request through your system should carry the same requestId. Without it, debugging a distributed flow is guessing.
// Generate at the edge (or trust X-Request-Id if upstream sends it)
const requestId = req.headers.get("x-request-id") ?? crypto.randomUUID();
const log = logger.child({ requestId, userId: session?.user.id });
log.info("request_started", { method: req.method, path: url.pathname });
// ... handler logic
log.info("request_completed", { status: 200, durationMs: 124 });
logger.child({...}) returns a logger where every line includes the parent fields. Use it per-request.
8. Edge Functions (Deno / Supabase)
// supabase/functions/_shared/log.ts
const POSTHOG_KEY = Deno.env.get("POSTHOG_API_KEY")!;
export async function logEvent(event: string, props: Record<string, unknown>) {
// Always echo to console for Supabase log drain
console.log(JSON.stringify({ event, ...props, ts: new Date().toISOString() }));
// Ship to PostHog for analytics
await fetch("https://app.posthog.com/capture/", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
api_key: POSTHOG_KEY,
event,
properties: props,
timestamp: new Date().toISOString(),
}),
}).catch(() => {}); // never let logging take down the request
}
Rule: logging failures must never break the request. Wrap shipping calls in try/catch or .catch(() => {}).
9. Browser (PostHog)
// src/lib/analytics.ts
import posthog from "posthog-js";
posthog.init(import.meta.env.VITE_POSTHOG_KEY, {
api_host: "https://app.posthog.com",
capture_pageview: true,
session_recording: { maskAllInputs: true }, // mask form fields by default
});
// Usage — same event-name + props shape as server
posthog.capture("checkout_completed", { orderId, valueCents });
Rules:
maskAllInputs: truefor session recording — never replay passwords- Identify users on auth:
posthog.identify(userId, { plan: "pro" }) - Reset on logout:
posthog.reset()— prevents cross-account leak
10. Quick Checklist
[ ] No console.log in production code (lint rule: no-console)
[ ] Logger configured with redact paths for secrets
[ ] Structured JSON in prod, pretty in dev
[ ] Event name first, context object second
[ ] requestId on every request, propagated via logger.child()
[ ] No PII (passwords, emails, tokens, SSN) in logs
[ ] Errors logged with .err (auto-serialized stack)
[ ] PostHog inputs masked for session replay
[ ] Logging failures swallowed — never break the request