Problem
Today, only unhandled errors caught by the Express 4-arg error middleware (lib/services/errorTracker.js:setupExpressErrorHandler) reach PostHog Error Tracking. Every logger.error(...) call inside a try/catch or a .catch(err => …) lives in stdout only.
Quantified gap:
pierreb-devkit/Node : 87 logger.error sites
comes-io/trawl_node (downstream after propagation) : 187 sites
Paradox: Vue side already forwards uncaught errors + unhandled rejections via captureException in src/main.js. Node is less instrumented than the frontend.
Spec
Custom Winston transport in lib/services/logger.posthog.transport.js:
import Transport from 'winston-transport';
import errorTracker from './errorTracker.js';
export class PostHogErrorTransport extends Transport {
constructor(opts = {}) {
super({ ...opts, level: 'error' }); // error + fatal only
}
log(info, callback) {
setImmediate(() => this.emit('logged', info));
if (info.posthogCaptured) return callback(); // de-dupe with express middleware
const err = info instanceof Error ? info
: info.error instanceof Error ? info.error
: Object.assign(new Error(info.message), { stack: info.stack });
errorTracker.captureException(err, {
distinctId: info.distinctId ?? 'system',
requestId: info.requestId,
logMessage: info.message,
logLevel: info.level,
});
callback();
}
}
Wire in lib/services/logger.js:
if (config.analytics?.posthog?.errorTracking === true) {
transports.push(new PostHogErrorTransport());
}
Mark the express middleware path so the transport skips dual-capture:
// in errorTracker.setupExpressErrorHandler
captureException(err, { distinctId, requestId: req.id });
err.posthogCaptured = true;
next(err);
Acceptance criteria
Gotchas to address in PR
- Double-capture :
posthogCaptured flag mitigation. Verify also for the errorTracker.captureException direct call path (out-of-band, no next(err)).
- PII :
logger.error('Failed to charge ${email}') would leak email to PostHog. Two options for now:
- Document the convention "no PII in error messages" in CLAUDE.md and accept it pre-launch
- Add a minimal redactor (regex for emails, JWT shapes, Stripe IDs) — defer if more than a few hours
- Volume : 274 sites × N invocations/day. Free tier 100k exceptions/mo should fit pre-PMF. Watch for noisy callers (consider adding a rate limiter in the transport if a single fingerprint exceeds N/min).
- Fingerprint : pose
\$exception_fingerprint via info.fingerprint ?? hash(err.stack first frame) so PostHog Error Tracking UI groups properly. Currently events don't group well (cf manual validation 2026-05-11).
- Loop risk : if PostHog SDK itself throws on capture, transport must NOT re-trigger via Winston. The SDK has internal try/catch already; verify the wrapper doesn't bubble.
Out of scope
- Forwarding
warn/info/debug levels to PostHog Logs (separate product, separate integration, not needed pre-PMF).
- Sourcemaps upload CI (separate plan section A.2 in
infra/docs/superpowers/plans/2026-05-10-posthog-observability-followups.md).
Context
Follow-up to:
Discovered 2026-05-11 during end-to-end validation: PostHog Error Tracking UI showed empty, root cause = no organic unhandled errors + handled errors invisible.
Problem
Today, only unhandled errors caught by the Express 4-arg error middleware (
lib/services/errorTracker.js:setupExpressErrorHandler) reach PostHog Error Tracking. Everylogger.error(...)call inside atry/catchor a.catch(err => …)lives in stdout only.Quantified gap:
pierreb-devkit/Node: 87logger.errorsitescomes-io/trawl_node(downstream after propagation) : 187 sitesParadox: Vue side already forwards uncaught errors + unhandled rejections via
captureExceptioninsrc/main.js. Node is less instrumented than the frontend.Spec
Custom Winston transport in
lib/services/logger.posthog.transport.js:Wire in
lib/services/logger.js:Mark the express middleware path so the transport skips dual-capture:
Acceptance criteria
PostHogErrorTransportclass shipped + unit tests (info-as-Error, info-with-error-property, info-as-string-message, dedupe viaposthogCapturedflag)logger.jsgated onconfig.analytics.posthog.errorTracking === trueerr.posthogCaptured = trueafter capturelogger.error('boom', err)from a fake module → assert a single$exceptionevent hits the PostHog client mockGotchas to address in PR
posthogCapturedflag mitigation. Verify also for theerrorTracker.captureExceptiondirect call path (out-of-band, nonext(err)).logger.error('Failed to charge ${email}')would leak email to PostHog. Two options for now:\$exception_fingerprintviainfo.fingerprint ?? hash(err.stack first frame)so PostHog Error Tracking UI groups properly. Currently events don't group well (cf manual validation 2026-05-11).Out of scope
warn/info/debuglevels to PostHog Logs (separate product, separate integration, not needed pre-PMF).infra/docs/superpowers/plans/2026-05-10-posthog-observability-followups.md).Context
Follow-up to:
Discovered 2026-05-11 during end-to-end validation: PostHog Error Tracking UI showed empty, root cause = no organic unhandled errors + handled errors invisible.