Hono

Contents

PostHog makes it easy to get data about usage of your Hono app. Integrating PostHog into your app enables analytics, custom events capture, feature flags, and more.

This guide walks you through integrating PostHog with your Hono app running on the Node.js runtime, but has also been tested with Cloudflare Workers, Vercel, and Bun. You can hook into the same middleware and onError for other runtimes to capture events.

Prerequisites

To follow this guide along, you need:

  1. A PostHog instance (either Cloud or self-hosted)
  2. A Hono application. We suggest trying first with a fresh Hono Node.js starter template.

Installation

Start by installing the posthog-node package using your package manager.

Terminal
npm install --save posthog-node

Then, create a middleware to capture events for your routes. Remember to export your project token as an environment variable.

index.ts
import { env } from 'hono/adapter'
import { createMiddleware } from 'hono/factory'
import { PostHog } from 'posthog-node'
const posthog = new PostHog(process.env.POSTHOG_PUBLIC_KEY, { host: 'https://us.i.posthog.com' })
const posthogServerMiddleware = createMiddleware(async (c, next) => {
posthog.capture({
distinctId: 'distinct_id_of_user', // Their user id or email
event: 'user_did_something',
})
await next()
await posthog.flush()
})
Shutdown

Keep one PostHog client for the Node.js application. Call await posthog.shutdown() once during application cleanup, after requests stop. Use flush() when an invocation must wait for buffered events, without shutting down the shared client.

Next, apply this middleware to your app. This middleware will be called on every request handled by Hono, denoted by the wildcard *.

index.ts
// use the middleware on all (*) routes.
app.use('*', posthogServerMiddleware)

In your routes, you can pass information to be captured by your middleware using context set() and get() pair

Cloudflare Pages

Cloudflare Pages uses its own middleware system that is different from Hono's middleware. Follow the Hono documentation on Cloudflare Pages to handle middleware.

Error tracking

Hono uses app.onError for root-level error handling. You can take advantage of this for Error tracking.

Reuse the existing client in your application's error handler:

index.ts
app.onError(async (err, c) => {
await posthog.captureExceptionImmediate(err, undefined, {
path: c.req.path,
method: c.req.method,
// ... other properties
})
await posthog.flush()
// other error handling logic
return c.text('Internal Server Error', 500)
})

Trace context on Node.js

For Node.js, register the optional context processor on your existing OpenTelemetry provider before requests start. Reuse the existing posthog client. Keep your exporter and sampler, and put tracing middleware outside the PostHog middleware.

Configure tracing_headers for your backend hostname. Replace the earlier posthogServerMiddleware with this middleware and register it before your routes:

JavaScript
import { enrichCurrentSpan } from 'posthog-node/otel'
function readTracingId(value) {
return value?.replace(/[\x00-\x1f\x7f-\x9f]/g, '').trim().slice(0, 1000) || undefined
}
const posthogServerMiddleware = createMiddleware(async (c, next) => {
const controller = new AbortController()
const user = c.get('user')
return posthog.withContext(
{
distinctId: user?.id ?? readTracingId(c.req.header('x-posthog-distinct-id')),
sessionId: readTracingId(c.req.header('x-posthog-session-id')),
},
async () => {
try {
enrichCurrentSpan(posthog)
await next()
} finally {
controller.abort()
}
},
{ fresh: true, signal: controller.signal }
)
})

The optional user value belongs to your application. Set it in earlier authentication middleware with a verified string id, or omit it and use header fallback for analytics. Header IDs are client-controlled analytics context, never authentication or authorization.

The helper enriches an active request span; the processor snapshots IDs for child spans. Existing canonical sessionId and posthogDistinctId attributes take precedence. Missing IDs stay absent. The scope ends after next() returns, including failures. Work that starts later, including stream body production, needs its own explicit context.

This automatic context setup requires the Node.js entry point. The SDK's edge entry point does not provide async request context. Explicit event properties in other runtimes are a separate path; this example does not establish Workers span support.

Next steps

To read more about how to integrate specific PostHog features into Hono, have a look at our Node SDK docs for concepts such as:

Still have questions?

Was this page useful?