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

sentry-integration.mddocs/

Sentry Integration

Integrate PostHog with Sentry to connect error tracking with user analytics. This integration adds direct links to PostHog users in Sentry, captures exceptions as PostHog events, and enriches error data with user context.

Capabilities

PostHogSentryIntegration Class (Sentry v7)

Class-based integration for Sentry v7 that automatically links Sentry errors to PostHog users.

/**
 * PostHog integration for Sentry v7 (class-based)
 * @param posthog - PostHog client instance
 * @param organization - Optional: Sentry organization name for direct links
 * @param prefix - Optional: URL prefix for self-hosted Sentry (default: 'https://sentry.io/organizations/')
 * @param severityAllowList - Optional: Array of severity levels to track, or '*' for all (default: ['error'])
 * @param sendExceptionsToPostHog - Optional: Whether to capture exceptions in PostHog (default: true)
 */
class PostHogSentryIntegration implements SentryIntegration {
  constructor(
    posthog: PostHog,
    organization?: string,
    prefix?: string,
    severityAllowList?: SeverityLevel[] | '*',
    sendExceptionsToPostHog?: boolean
  );

  static readonly POSTHOG_ID_TAG: string = 'posthog_distinct_id';
}

Usage Examples:

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

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

// Basic setup
Sentry.init({
  dsn: 'https://examplePublicKey@o0.ingest.sentry.io/0',
  integrations: [
    new PostHogSentryIntegration(client)
  ]
});

// Set the PostHog user ID on Sentry events
Sentry.setTag(PostHogSentryIntegration.POSTHOG_ID_TAG, 'user_123');

// Now when errors occur, they'll be linked to the PostHog user
throw new Error('Something went wrong');

sentryIntegration Function (Sentry v8)

Function-based integration for Sentry v8 that provides the same functionality as the class-based integration.

/**
 * PostHog integration for Sentry v8 (function-based)
 * @param posthog - PostHog client instance
 * @param options - Configuration options
 * @returns Sentry integration object
 */
function sentryIntegration(
  posthog: PostHog,
  options?: SentryIntegrationOptions
): SentryIntegration;

interface SentryIntegrationOptions {
  /** Sentry organization name for direct links */
  organization?: string;
  /** Sentry project ID for direct links */
  projectId?: number;
  /** URL prefix for self-hosted Sentry (default: 'https://sentry.io/organizations/') */
  prefix?: string;
  /** Array of severity levels to track, or '*' for all (default: ['error']) */
  severityAllowList?: SeverityLevel[] | '*';
  /** Whether to capture exceptions in PostHog (default: true) */
  sendExceptionsToPostHog?: boolean;
}

type SeverityLevel = 'fatal' | 'error' | 'warning' | 'log' | 'info' | 'debug';

Usage Examples:

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

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

// Basic setup
Sentry.init({
  dsn: 'https://examplePublicKey@o0.ingest.sentry.io/0',
  integrations: [
    sentryIntegration(client)
  ]
});

// Set the PostHog user ID on Sentry events
Sentry.setTag(PostHogSentryIntegration.POSTHOG_ID_TAG, 'user_123');

// With options
Sentry.init({
  dsn: 'https://examplePublicKey@o0.ingest.sentry.io/0',
  integrations: [
    sentryIntegration(client, {
      organization: 'my-org',
      projectId: 12345,
      severityAllowList: ['error', 'fatal']
    })
  ]
});

POSTHOG_ID_TAG Constant

The Sentry tag key used to identify PostHog users in Sentry events.

/**
 * The Sentry tag key for PostHog distinct IDs
 */
const POSTHOG_ID_TAG: string = 'posthog_distinct_id';

Setup Examples

Sentry v7 Setup

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

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

// Initialize Sentry with PostHog integration
Sentry.init({
  dsn: 'https://examplePublicKey@o0.ingest.sentry.io/0',
  integrations: [
    new PostHogSentryIntegration(
      client,
      'my-organization', // Sentry organization
      undefined,         // Use default prefix
      ['error', 'fatal'], // Only track errors and fatal issues
      true               // Send exceptions to PostHog
    )
  ],
  tracesSampleRate: 1.0
});

// In your application code, set the user ID
function handleRequest(userId: string) {
  // Set PostHog distinct ID for Sentry
  Sentry.setTag(PostHogSentryIntegration.POSTHOG_ID_TAG, userId);

  // Your code here
  processUserRequest(userId);
}

Sentry v8 Setup

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

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

// Initialize Sentry with PostHog integration
Sentry.init({
  dsn: 'https://examplePublicKey@o0.ingest.sentry.io/0',
  integrations: [
    sentryIntegration(client, {
      organization: 'my-organization',
      projectId: 12345,
      severityAllowList: ['error', 'fatal'],
      sendExceptionsToPostHog: true
    })
  ],
  tracesSampleRate: 1.0
});

// In your application code, set the user ID
function handleRequest(userId: string) {
  // Set PostHog distinct ID for Sentry
  Sentry.setTag(PostHogSentryIntegration.POSTHOG_ID_TAG, userId);

  // Your code here
  processUserRequest(userId);
}

Express.js Integration

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

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

// Initialize Sentry with PostHog
Sentry.init({
  dsn: 'https://examplePublicKey@o0.ingest.sentry.io/0',
  integrations: [
    new Sentry.Integrations.Http({ tracing: true }),
    new Sentry.Integrations.Express({ app }),
    sentryIntegration(client, {
      organization: 'my-org',
      projectId: 12345
    })
  ]
});

// Sentry request handler (must be first)
app.use(Sentry.Handlers.requestHandler());
app.use(Sentry.Handlers.tracingHandler());

// Middleware to set PostHog user ID
app.use((req, res, next) => {
  if (req.user?.id) {
    Sentry.setTag(PostHogSentryIntegration.POSTHOG_ID_TAG, req.user.id);
  }
  next();
});

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

// Sentry error handler (must be after routes)
app.use(Sentry.Handlers.errorHandler());

// Start server
app.listen(3000);

Next.js Integration

// sentry.server.config.ts
import * as Sentry from '@sentry/nextjs';
import { PostHog, sentryIntegration } from 'posthog-node';

const client = new PostHog(process.env.NEXT_PUBLIC_POSTHOG_KEY!, {
  host: process.env.NEXT_PUBLIC_POSTHOG_HOST
});

Sentry.init({
  dsn: process.env.SENTRY_DSN,
  integrations: [
    sentryIntegration(client, {
      organization: 'my-org',
      projectId: Number(process.env.SENTRY_PROJECT_ID)
    })
  ],
  tracesSampleRate: 1.0
});

// In your API routes or server components
import { PostHogSentryIntegration } from 'posthog-node';
import * as Sentry from '@sentry/nextjs';

export async function GET(request: Request) {
  const userId = await getUserId(request);

  if (userId) {
    Sentry.setTag(PostHogSentryIntegration.POSTHOG_ID_TAG, userId);
  }

  // Your API logic
}

Linking Users to Errors

Setting User Context

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

const client = new PostHog('phc_your_api_key');

// Method 1: Set tag directly
Sentry.setTag(PostHogSentryIntegration.POSTHOG_ID_TAG, 'user_123');

// Method 2: Set in scope
Sentry.configureScope((scope) => {
  scope.setTag(PostHogSentryIntegration.POSTHOG_ID_TAG, 'user_123');
});

// Method 3: Set with context
Sentry.setContext('posthog', {
  [PostHogSentryIntegration.POSTHOG_ID_TAG]: 'user_123'
});

// Method 4: Set in error capture
Sentry.captureException(error, {
  tags: {
    [PostHogSentryIntegration.POSTHOG_ID_TAG]: 'user_123'
  }
});

Dynamic User Context

import * as Sentry from '@sentry/node';
import { PostHogSentryIntegration } from 'posthog-node';

// Set user context per request
async function handleRequest(req: Request) {
  const userId = await getUserIdFromRequest(req);

  if (userId) {
    // Set for this scope
    Sentry.setTag(PostHogSentryIntegration.POSTHOG_ID_TAG, userId);

    // Also identify in PostHog
    client.identify({
      distinctId: userId,
      properties: {
        email: req.user.email,
        name: req.user.name
      }
    });
  }

  try {
    await processRequest(req);
  } catch (error) {
    // Error will be linked to userId in both Sentry and PostHog
    throw error;
  }
}

Anonymous vs Identified Users

import * as Sentry from '@sentry/node';
import { PostHogSentryIntegration } from 'posthog-node';

function setUserContext(user: User | null) {
  if (user) {
    // Identified user
    Sentry.setTag(PostHogSentryIntegration.POSTHOG_ID_TAG, user.id);
    Sentry.setUser({
      id: user.id,
      email: user.email,
      username: user.username
    });
  } else {
    // Anonymous user - use session ID or device ID
    const sessionId = getSessionId();
    Sentry.setTag(PostHogSentryIntegration.POSTHOG_ID_TAG, sessionId);
    Sentry.setUser({
      id: sessionId
    });
  }
}

What Gets Captured

In Sentry

When an error occurs, the following PostHog data is added to Sentry:

{
  tags: {
    'posthog_distinct_id': 'user_123',
    'PostHog Person URL': 'https://app.posthog.com/project/12345/person/user_123'
  }
}

In PostHog

When an error occurs, a $exception event is captured with these properties:

{
  event: '$exception',
  distinctId: 'user_123',
  properties: {
    // PostHog exception properties
    $exception_message: 'Error message',
    $exception_type: 'Error',
    $exception_level: 'error',
    $exception_list: [{
      type: 'Error',
      value: 'Error message',
      stacktrace: {
        frames: [/* stack frames */]
      }
    }],

    // Sentry exception properties
    $sentry_event_id: '80a7023ac32c47f7acb0adaed600d149',
    $sentry_exception: {/* Sentry exception data */},
    $sentry_exception_message: 'Error message',
    $sentry_exception_type: 'Error',
    $sentry_tags: {/* All Sentry tags */},
    $sentry_url: 'https://sentry.io/organizations/my-org/issues/?project=12345&query=80a7023ac32c47f7acb0adaed600d149'
  }
}

Configuration Options

Severity Allow List

Control which error severity levels are tracked:

// Track only errors and fatal issues (default)
sentryIntegration(client, {
  severityAllowList: ['error', 'fatal']
});

// Track all severity levels
sentryIntegration(client, {
  severityAllowList: '*'
});

// Track errors, warnings, and info
sentryIntegration(client, {
  severityAllowList: ['error', 'warning', 'info']
});

// Available levels: 'fatal', 'error', 'warning', 'log', 'info', 'debug'

Organization and Project Links

Create direct links from PostHog to Sentry issues:

// With organization and project ID
sentryIntegration(client, {
  organization: 'my-company',
  projectId: 12345
});
// Creates links like: https://sentry.io/organizations/my-company/issues/?project=12345&query=<event_id>

// Self-hosted Sentry
sentryIntegration(client, {
  organization: 'my-company',
  projectId: 12345,
  prefix: 'https://sentry.mycompany.com/organizations/'
});
// Creates links like: https://sentry.mycompany.com/organizations/my-company/issues/?project=12345&query=<event_id>

Disable PostHog Exception Events

Only add PostHog links to Sentry, don't capture exceptions in PostHog:

sentryIntegration(client, {
  sendExceptionsToPostHog: false
});

This still adds the PostHog person URL to Sentry events, but doesn't create $exception events in PostHog.

Advanced Usage

Multi-tenant Applications

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

const clients = new Map<string, PostHog>();

function getClientForTenant(tenantId: string): PostHog {
  if (!clients.has(tenantId)) {
    clients.set(tenantId, new PostHog(
      getTenantApiKey(tenantId),
      { host: 'https://app.posthog.com' }
    ));
  }
  return clients.get(tenantId)!;
}

// Initialize Sentry with default client
const defaultClient = getClientForTenant('default');
Sentry.init({
  dsn: process.env.SENTRY_DSN,
  integrations: [
    sentryIntegration(defaultClient)
  ]
});

// Set user context per request
app.use((req, res, next) => {
  const tenantId = req.headers['x-tenant-id'];
  const userId = req.user?.id;

  if (userId) {
    // Use tenant-specific format for user ID
    const distinctId = `${tenantId}:${userId}`;
    Sentry.setTag(PostHogSentryIntegration.POSTHOG_ID_TAG, distinctId);

    // Track in tenant-specific PostHog instance
    const client = getClientForTenant(tenantId);
    client.identify({
      distinctId,
      properties: {
        tenant_id: tenantId,
        user_id: userId
      }
    });
  }

  next();
});

Conditional Tracking

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

const client = new PostHog('phc_your_api_key');

// Only track certain error types
sentryIntegration(client, {
  severityAllowList: ['error', 'fatal']
});

// Custom filtering
Sentry.init({
  dsn: process.env.SENTRY_DSN,
  integrations: [
    sentryIntegration(client)
  ],
  beforeSend(event, hint) {
    // Don't send to PostHog for certain errors
    if (event.exception?.values?.[0]?.type === 'ValidationError') {
      delete event.tags?.[PostHogSentryIntegration.POSTHOG_ID_TAG];
    }
    return event;
  }
});

Integration with Feature Flags

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

const client = new PostHog('phc_your_api_key');

Sentry.init({
  dsn: process.env.SENTRY_DSN,
  integrations: [
    sentryIntegration(client, {
      organization: 'my-org',
      projectId: 12345
    })
  ]
});

async function handleRequest(userId: string) {
  // Set user context
  Sentry.setTag(PostHogSentryIntegration.POSTHOG_ID_TAG, userId);

  // Get feature flags
  const flags = await client.getAllFlags(userId);

  // Add feature flags to Sentry context
  Sentry.setContext('feature_flags', flags);

  try {
    // Process request with feature flags
    if (flags['new-feature']) {
      await processWithNewFeature(userId);
    } else {
      await processWithOldFeature(userId);
    }
  } catch (error) {
    // Error will include both user ID and active feature flags
    throw error;
  }
}

Graceful Shutdown

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

const client = new PostHog('phc_your_api_key');

Sentry.init({
  dsn: process.env.SENTRY_DSN,
  integrations: [
    sentryIntegration(client)
  ]
});

// Graceful shutdown
async function shutdown() {
  console.log('Shutting down...');

  // Flush Sentry events
  await Sentry.close(2000);

  // Flush PostHog events
  await client.shutdown(5000);

  process.exit(0);
}

process.on('SIGTERM', shutdown);
process.on('SIGINT', shutdown);

Best Practices

1. Always Set User Context

Set the PostHog distinct ID for every request or operation to ensure errors are properly linked:

// Good: Set user context at request start
app.use((req, res, next) => {
  if (req.user?.id) {
    Sentry.setTag(PostHogSentryIntegration.POSTHOG_ID_TAG, req.user.id);
  }
  next();
});

// Bad: Only set on error
app.use((error, req, res, next) => {
  if (req.user?.id) {
    Sentry.setTag(PostHogSentryIntegration.POSTHOG_ID_TAG, req.user.id);
  }
  next(error);
});

2. Use Consistent User IDs

Use the same distinct ID format across PostHog and Sentry:

// Good: Consistent ID format
const userId = `user_${user.id}`;
Sentry.setTag(PostHogSentryIntegration.POSTHOG_ID_TAG, userId);
client.identify({ distinctId: userId });

// Bad: Different ID formats
Sentry.setTag(PostHogSentryIntegration.POSTHOG_ID_TAG, user.id);
client.identify({ distinctId: `user_${user.id}` });

3. Configure Organization and Project

Always provide organization and project ID for direct Sentry links:

// Good: Direct links to Sentry
sentryIntegration(client, {
  organization: 'my-org',
  projectId: 12345
});

// Missing: No direct links
sentryIntegration(client);

4. Filter by Severity

Only track relevant error levels to reduce noise:

// Good: Track only errors and fatal issues
sentryIntegration(client, {
  severityAllowList: ['error', 'fatal']
});

// Bad: Track everything including debug logs
sentryIntegration(client, {
  severityAllowList: '*'
});

5. Handle Anonymous Users

Set context for both authenticated and anonymous users:

// Good: Handle both cases
function setUserContext(req: Request) {
  const userId = req.user?.id || req.session?.id || generateAnonymousId();
  Sentry.setTag(PostHogSentryIntegration.POSTHOG_ID_TAG, userId);
}

// Bad: Only authenticated users
function setUserContext(req: Request) {
  if (req.user?.id) {
    Sentry.setTag(PostHogSentryIntegration.POSTHOG_ID_TAG, req.user.id);
  }
}

6. Clean Up in Isolated Contexts

Clear user context after processing to avoid leakage:

// Good: Clean up after processing
async function processJob(job: Job) {
  Sentry.configureScope((scope) => {
    scope.setTag(PostHogSentryIntegration.POSTHOG_ID_TAG, job.userId);
  });

  try {
    await job.execute();
  } finally {
    Sentry.configureScope((scope) => {
      scope.setTag(PostHogSentryIntegration.POSTHOG_ID_TAG, undefined);
    });
  }
}

// Bad: Context leaks to next job
async function processJob(job: Job) {
  Sentry.setTag(PostHogSentryIntegration.POSTHOG_ID_TAG, job.userId);
  await job.execute();
}

7. Test Integration

Verify the integration works correctly:

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

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

Sentry.init({
  dsn: process.env.SENTRY_DSN,
  integrations: [
    sentryIntegration(client, {
      organization: 'my-org',
      projectId: 12345
    })
  ]
});

// Test function
async function testIntegration() {
  const testUserId = 'test_user_' + Date.now();

  // Set user context
  Sentry.setTag(PostHogSentryIntegration.POSTHOG_ID_TAG, testUserId);

  // Capture test exception
  try {
    throw new Error('Test exception from integration');
  } catch (error) {
    Sentry.captureException(error);
  }

  // Wait for events to be sent
  await client.shutdown();
  await Sentry.close(2000);

  console.log(`Check Sentry for event with PostHog user: ${testUserId}`);
  console.log(`Check PostHog for $exception event for user: ${testUserId}`);
}

testIntegration();

8. Monitor Integration Health

Track integration metrics:

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

const client = new PostHog('phc_your_api_key');

let integrationErrors = 0;
let eventsProcessed = 0;

Sentry.init({
  dsn: process.env.SENTRY_DSN,
  integrations: [
    sentryIntegration(client)
  ],
  beforeSend(event, hint) {
    eventsProcessed++;

    // Check if PostHog user is set
    if (!event.tags?.[PostHogSentryIntegration.POSTHOG_ID_TAG]) {
      integrationErrors++;
      console.warn('Sentry event missing PostHog user ID');
    }

    return event;
  }
});

// Report metrics periodically
setInterval(() => {
  client.capture({
    event: 'sentry_integration_metrics',
    distinctId: 'system',
    properties: {
      events_processed: eventsProcessed,
      integration_errors: integrationErrors,
      error_rate: integrationErrors / eventsProcessed
    }
  });
}, 60000);

Troubleshooting

No PostHog User URL in Sentry

If the PostHog person URL isn't appearing in Sentry:

  1. Verify the PostHog distinct ID is set:
Sentry.setTag(PostHogSentryIntegration.POSTHOG_ID_TAG, userId);
  1. Check the tag name is correct:
console.log('Tag:', PostHogSentryIntegration.POSTHOG_ID_TAG); // 'posthog_distinct_id'
  1. Ensure the integration is initialized:
Sentry.init({
  integrations: [sentryIntegration(client)]
});

No $exception Events in PostHog

If exceptions aren't appearing in PostHog:

  1. Verify sendExceptionsToPostHog is enabled:
sentryIntegration(client, {
  sendExceptionsToPostHog: true // default
});
  1. Check severity allow list:
sentryIntegration(client, {
  severityAllowList: ['error', 'fatal'] // or '*' for all
});
  1. Ensure PostHog client is initialized:
const client = new PostHog('phc_your_api_key');

Wrong User Linked in Sentry

If errors are linked to the wrong user:

  1. Clear scope between requests:
// Use request-scoped context
app.use((req, res, next) => {
  Sentry.withScope((scope) => {
    scope.setTag(PostHogSentryIntegration.POSTHOG_ID_TAG, req.user?.id);
    next();
  });
});
  1. Use Sentry hubs for isolation:
const hub = Sentry.getCurrentHub();
hub.configureScope((scope) => {
  scope.setTag(PostHogSentryIntegration.POSTHOG_ID_TAG, userId);
});

Self-Hosted Sentry Links Not Working

If links to self-hosted Sentry are incorrect:

  1. Set the correct prefix:
sentryIntegration(client, {
  organization: 'my-org',
  projectId: 12345,
  prefix: 'https://sentry.mycompany.com/organizations/'
});
  1. Verify the URL format matches your Sentry instance:
// Should produce: https://sentry.mycompany.com/organizations/my-org/issues/?project=12345&query=<event_id>