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.
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');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']
})
]
});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';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);
}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);
}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);// 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
}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'
}
});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;
}
}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
});
}
}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'
}
}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'
}
}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'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>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.
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();
});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;
}
});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;
}
}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);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);
});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}` });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);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: '*'
});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);
}
}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();
}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();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);If the PostHog person URL isn't appearing in Sentry:
Sentry.setTag(PostHogSentryIntegration.POSTHOG_ID_TAG, userId);console.log('Tag:', PostHogSentryIntegration.POSTHOG_ID_TAG); // 'posthog_distinct_id'Sentry.init({
integrations: [sentryIntegration(client)]
});If exceptions aren't appearing in PostHog:
sendExceptionsToPostHog is enabled:sentryIntegration(client, {
sendExceptionsToPostHog: true // default
});sentryIntegration(client, {
severityAllowList: ['error', 'fatal'] // or '*' for all
});const client = new PostHog('phc_your_api_key');If errors are linked to the wrong user:
// Use request-scoped context
app.use((req, res, next) => {
Sentry.withScope((scope) => {
scope.setTag(PostHogSentryIntegration.POSTHOG_ID_TAG, req.user?.id);
next();
});
});const hub = Sentry.getCurrentHub();
hub.configureScope((scope) => {
scope.setTag(PostHogSentryIntegration.POSTHOG_ID_TAG, userId);
});If links to self-hosted Sentry are incorrect:
sentryIntegration(client, {
organization: 'my-org',
projectId: 12345,
prefix: 'https://sentry.mycompany.com/organizations/'
});// Should produce: https://sentry.mycompany.com/organizations/my-org/issues/?project=12345&query=<event_id>