or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

client-configuration.mderror-tracking.mdevent-tracking.mdexperimental.mdexpress-integration.mdfeature-flags.mdidentification.mdindex.mdsentry-integration.md
tile.json

express-integration.mddocs/

Express Integration

Automatic error tracking middleware for Express.js applications.

Capabilities

Setup Express Error Handler

Set up Express middleware to automatically capture uncaught errors in your application.

/**
 * Set up Express error handler middleware
 * @param posthog - PostHog client instance
 * @param app - Express application or router with use() method
 */
function setupExpressErrorHandler(
  posthog: PostHog,
  app: { use: (middleware: ExpressMiddleware | ExpressErrorMiddleware) => unknown }
): void;

Usage Examples:

import express from 'express';
import { PostHog, setupExpressErrorHandler } from 'posthog-node';

const app = express();
const client = new PostHog('phc_your_api_key');

// Your routes
app.get('/api/users', async (req, res) => {
  const users = await getUsers();
  res.json(users);
});

// Setup error handler (must be after all routes)
setupExpressErrorHandler(client, app);

// Start server
app.listen(3000);

How It Works

The Express error handler middleware:

  1. Catches all errors that occur in your Express routes
  2. Extracts error status codes from various error formats
  3. Captures the error to PostHog with personless processing
  4. Passes the error to the next error handler (doesn't swallow errors)
  5. Uses a synthetic distinct ID (UUID) since no user context is available

Complete Setup Example

import express from 'express';
import { PostHog, setupExpressErrorHandler } from 'posthog-node';

const app = express();
const client = new PostHog('phc_your_api_key', {
  host: 'https://app.posthog.com'
});

// Middleware
app.use(express.json());
app.use(express.urlencoded({ extended: true }));

// Routes
app.get('/', (req, res) => {
  res.json({ status: 'ok' });
});

app.get('/api/users/:id', async (req, res) => {
  const user = await getUser(req.params.id);
  if (!user) {
    throw new Error('User not found');
  }
  res.json(user);
});

app.post('/api/orders', async (req, res) => {
  const order = await createOrder(req.body);
  res.json(order);
});

// Setup PostHog error handler (after routes, before other error handlers)
setupExpressErrorHandler(client, app);

// Custom error handler (optional, after PostHog handler)
app.use((error, req, res, next) => {
  console.error('Error:', error);
  res.status(error.status || 500).json({
    error: error.message || 'Internal server error'
  });
});

// Start server
const server = app.listen(3000, () => {
  console.log('Server listening on port 3000');
});

// Graceful shutdown
process.on('SIGTERM', async () => {
  server.close();
  await client.shutdown();
  process.exit(0);
});

Middleware Order

The order of middleware is important in Express. PostHog error handler should be:

  1. After all routes - So it can catch errors from your routes
  2. Before custom error handlers - So it captures errors first
  3. After other middleware - After body parsers, CORS, etc.
// 1. Basic middleware
app.use(express.json());
app.use(cors());

// 2. Routes
app.get('/api/users', getUsers);
app.post('/api/orders', createOrder);

// 3. PostHog error handler
setupExpressErrorHandler(client, app);

// 4. Custom error handlers
app.use(customErrorHandler);

Error Status Handling

The middleware automatically extracts status codes from various error formats:

// Standard Express error
const error = new Error('Not found');
error.status = 404; // Captured

// HTTP errors (http-errors package)
const error = createError(404, 'Not found');
error.statusCode = 404; // Captured

// Custom error formats
const error = new Error('Bad request');
error.status_code = 400; // Captured

// Boom errors
const error = Boom.badRequest('Invalid input');
error.output.statusCode = 400; // Captured

With User Context

The setupExpressErrorHandler uses personless processing by default. For user-specific error tracking, use manual error capture:

import { PostHog, setupExpressErrorHandler } from 'posthog-node';

const client = new PostHog('phc_your_api_key');

// Manual error handling with user context
app.get('/api/protected', async (req, res) => {
  try {
    const data = await getProtectedData(req.user.id);
    res.json(data);
  } catch (error) {
    // Capture with user ID
    client.captureException(error, req.user.id, {
      endpoint: '/api/protected',
      method: req.method
    });
    throw error; // Will also be caught by setupExpressErrorHandler
  }
});

// Automatic error handling (personless)
setupExpressErrorHandler(client, app);

Async Error Handling

For async route handlers, ensure errors are properly caught:

// Using express-async-errors
import 'express-async-errors';

app.get('/api/users', async (req, res) => {
  // Errors automatically caught
  const users = await getUsers();
  res.json(users);
});

setupExpressErrorHandler(client, app);

// Or wrap in try-catch
app.get('/api/users', async (req, res, next) => {
  try {
    const users = await getUsers();
    res.json(users);
  } catch (error) {
    next(error); // Pass to error handler
  }
});

Multiple Express Apps

// Main app
const app = express();
const client = new PostHog('phc_your_api_key');

// API router
const apiRouter = express.Router();
apiRouter.get('/users', getUsers);
apiRouter.post('/orders', createOrders);

// Setup error handler on router
setupExpressErrorHandler(client, apiRouter);

// Mount router
app.use('/api', apiRouter);

// Main app error handler
setupExpressErrorHandler(client, app);

With Other Error Tracking

You can use PostHog alongside other error tracking services:

import { PostHog, setupExpressErrorHandler } from 'posthog-node';
import * as Sentry from '@sentry/node';

const client = new PostHog('phc_your_api_key');

// Sentry error handler
app.use(Sentry.Handlers.errorHandler());

// PostHog error handler
setupExpressErrorHandler(client, app);

// Custom error handler
app.use((error, req, res, next) => {
  res.status(500).json({ error: 'Internal error' });
});

Filtering Errors

To filter which errors are sent to PostHog, use manual error capture instead:

// Don't use setupExpressErrorHandler, use custom middleware
app.use((error, req, res, next) => {
  // Only capture 500 errors
  if (error.status >= 500) {
    client.captureException(error, req.user?.id, {
      endpoint: req.path,
      method: req.method,
      status: error.status
    });
  }

  next(error);
});

Development vs Production

const client = new PostHog('phc_your_api_key', {
  disabled: process.env.NODE_ENV === 'development'
});

// Only setup in production
if (process.env.NODE_ENV === 'production') {
  setupExpressErrorHandler(client, app);
}

// Or use both with different configs
if (process.env.NODE_ENV === 'development') {
  app.use((error, req, res, next) => {
    console.error('Development error:', error);
    next(error);
  });
} else {
  setupExpressErrorHandler(client, app);
}

Testing

import request from 'supertest';
import express from 'express';
import { PostHog, setupExpressErrorHandler } from 'posthog-node';

describe('Express error handling', () => {
  let app;
  let client;

  beforeEach(() => {
    app = express();
    client = new PostHog('test_key', { disabled: true });

    app.get('/error', () => {
      throw new Error('Test error');
    });

    setupExpressErrorHandler(client, app);
  });

  it('should handle errors', async () => {
    const response = await request(app).get('/error');
    expect(response.status).toBe(500);
  });
});

Best Practices

1. Place After All Routes

// Good
app.get('/api/users', getUsers);
app.post('/api/orders', createOrders);
setupExpressErrorHandler(client, app);

// Bad
setupExpressErrorHandler(client, app);
app.get('/api/users', getUsers); // Won't be caught

2. Use With Manual Capture for User Context

// Combine automatic and manual tracking
app.post('/api/checkout', async (req, res) => {
  try {
    await processCheckout(req.user.id, req.body);
    res.json({ success: true });
  } catch (error) {
    // Manual: with user context
    client.captureException(error, req.user.id, {
      step: 'checkout',
      cart_total: req.body.total
    });
    throw error; // Automatic: will also be caught
  }
});

setupExpressErrorHandler(client, app);

3. Shutdown Gracefully

const server = app.listen(3000);

process.on('SIGTERM', async () => {
  server.close();
  await client.shutdown(5000);
  process.exit(0);
});

4. Don't Rely Solely on Automatic Capture

Use manual captureException() for errors that need user context or additional properties:

app.post('/api/payment', async (req, res) => {
  try {
    await processPayment(req.body);
    res.json({ success: true });
  } catch (error) {
    // Capture with rich context
    client.captureException(error, req.user.id, {
      payment_method: req.body.method,
      amount: req.body.amount,
      order_id: req.body.orderId
    });
    res.status(500).json({ error: 'Payment failed' });
  }
});

Limitations

  • Uses personless processing (no user association by default)
  • Generates a synthetic UUID for each error
  • Cannot access request context (user, session, etc.) automatically
  • For user-specific tracking, use manual captureException()