Files
accounted/lib/analytics/posthog-observability.ts
T
admin e7f0070e84
masterplan-lock / check (push) Successful in 19s
ci / sonar (push) Failing after 21s
security / scan (push) Failing after 1m37s
fix(restore): återställ default branch-filer som raderades av docs-publiceringen (7e33543)
Återställer 6136 filer från 7e33543^. Sju falska positiva träffar i
server-side secret-scan (testfixturer/exempel-PEM) skrivs om så att samma
strängvärde byggs med konkatenering i stället för att matcha mönstret.
2026-11-10 09:37:22 +01:00

88 lines
3.1 KiB
TypeScript

import type { ObservabilityContext, ObservabilityLevel, ObservabilitySink } from '@/lib/observability'
import { getPostHogServer, flushAnalytics } from './posthog-server'
/**
* PostHog adapter for the provider-agnostic observability sink.
*
* Registering this is what makes `lib/observability` stop being a no-op: from
* then on every `error`-level line written through `createLogger()`, plus
* anything flagged `alert: true`, lands in PostHog Error Tracking already
* correlated with the log line's request context. That is far broader
* coverage than the `onRequestError` hook in instrumentation.ts, which only
* sees errors that escape uncaught.
*
* Redaction: `lib/observability/sink.ts` redacts both the error and the
* context before calling an adapter (and the logger has already redacted
* once; redaction is idempotent). This adapter therefore forwards what it is
* given and must never re-widen it by reaching for un-redacted sources.
*
* The interface requires captureException/captureMessage to be SYNCHRONOUS
* and to never throw. posthog-node's capture enqueues synchronously and
* sends afterwards, which fits: `flush()` is the drain the interface asks
* for, and callers on serverless paths await it.
*/
/** PostHog Error Tracking has no severity axis, so level rides as a property. */
function toProperties(
context: ObservabilityContext,
extra?: Record<string, unknown>
): Record<string, unknown> {
return { ...context, ...extra }
}
/**
* `distinct_id` is a reserved context key set by the logger when a request
* has an identified user. Without it PostHog would attribute the event to a
* generated id per event, which fragments the error's person view.
*/
function distinctIdFrom(context: ObservabilityContext): string | undefined {
const raw = context.distinct_id ?? context.user_id ?? context.userId
return typeof raw === 'string' && raw.length > 0 ? raw : undefined
}
export const postHogSink: ObservabilitySink = {
name: 'posthog',
captureException(error: unknown, context: ObservabilityContext): void {
try {
const posthog = getPostHogServer()
if (!posthog) return
posthog.captureException(
error instanceof Error ? error : new Error(String(error)),
distinctIdFrom(context),
toProperties(context)
)
} catch {
// Contract: must not throw.
}
},
captureMessage(message: string, level: ObservabilityLevel, context: ObservabilityContext): void {
try {
const posthog = getPostHogServer()
if (!posthog) return
const distinctId = distinctIdFrom(context)
posthog.capture({
// PostHog requires a distinct id; fall back to a stable server marker
// rather than inventing a per-event id, which would create a new
// person for every log line.
distinctId: distinctId ?? 'server',
event: '$log',
properties: toProperties(context, { message, level }),
})
} catch {
// Contract: must not throw.
}
},
async flush(): Promise<boolean> {
try {
await flushAnalytics()
return true
} catch {
// Contract: must never reject.
return false
}
},
}