Skip to main content

Audit logging

Every HTTP request gets a row in PostHog. Not optional.

Pipeline

What's logged

Each entry includes:

FieldSource
eventapi_request
requestIdUUID generated by LoggingInterceptor
userIdreq.user?.id
userRolereq.user?.role
orgIdreq.user?.activeOrgId if present
method, path, statusCodeStandard HTTP
resultsuccess or failure (4xx/5xx)
errorCodeerror.code if failure
duration_msMeasured by LoggingInterceptor
userAgent, ipFrom request headers

PII surface is intentional: no request bodies, no response bodies, no query params. Only metadata.

Flush triggers

flush when:
- queue.length >= LOG_BATCH_SIZE // default 100
- LOG_BATCH_INTERVAL_MS elapses // default 30000
- onApplicationShutdown // best-effort drain

Env

VarDefault
POSTHOG_API_KEY(none — batching silently disabled)
POSTHOG_HOSThttps://app.posthog.com
LOG_BATCH_SIZE100
LOG_BATCH_INTERVAL_MS30000

If POSTHOG_API_KEY is absent, the queue still receives entries (so dev environments can hold the line) but nothing leaves the process. No errors thrown.

Why PostHog, not Stripe-style append-only

PostHog gives free querying, dashboards, alerts, and retention. The trade-off is that you don't have a local immutable log. If that matters, mirror the queue to a local audit_logs table at the same time (the schema is there but currently unused by middleware).

Adding a business event

Don't add ad-hoc PostHog calls from controllers. Use the queue:

constructor(@Inject(AUDIT_LOGGER) private logger: AuditLoggerService) {}

async someMethod() {
// ... business logic ...
await this.logger.log({
event: "agent_published",
properties: { agentId, by: req.user.id },
});
}

This keeps batching, retry, and shutdown drain behavior uniform.

Direct querying

For ad-hoc investigation, the PostHog UI's Insights tab is the fastest. Common queries:

  • "Show all api_request with result = 'failure' in the last hour"
  • "Show top 20 paths by 5xx rate"
  • "Group api_request by userId for usage by user"

Sampling

Not currently sampled. At our scale this is fine. If request volume grows, sample 4xx/5xx fully and sample 2xx at 1–10%.

See also