Configure Sentry error tracking for Node.js applications with PM2 process management, automated error reporting, and production-ready alerting to catch and debug issues before they affect users.
Prerequisites
- Root or sudo access
- Basic knowledge of Node.js and npm
- Sentry account (free tier available)
What this solves
Node.js applications in production need comprehensive error tracking to catch runtime exceptions, performance issues, and user-affecting bugs before they cascade. Sentry provides real-time error monitoring with detailed stack traces, user context, and alerting that integrates seamlessly with Node.js applications running under PM2 process management.
Step-by-step installation
Update system packages
Start by updating your package manager to ensure you get the latest versions of Node.js and PM2.
sudo apt update && sudo apt upgrade -y
sudo apt install -y curl build-essential
Install Node.js and npm
Install the latest LTS version of Node.js using the NodeSource repository for better version control and security updates.
curl -fsSL https://deb.nodesource.com/setup_lts.x | sudo -E bash -
sudo apt install -y nodejs
Install PM2 globally
PM2 provides process management for Node.js applications with clustering, monitoring, and automatic restarts.
sudo npm install -g pm2
pm2 --version
Create Sentry account and project
Sign up for a Sentry account at sentry.io and create a new Node.js project. Copy the DSN (Data Source Name) from your project settings.
Create sample Node.js application
Create a basic Express.js application to demonstrate Sentry integration and error tracking capabilities.
mkdir -p /opt/nodeapp
cd /opt/nodeapp
npm init -y
Install required dependencies
Install Sentry SDK, Express framework, and additional packages for comprehensive error tracking.
npm install @sentry/node @sentry/profiling-node express helmet cors dotenv
npm install --save-dev nodemon
Configure environment variables
Create environment configuration file to store Sentry DSN and application settings securely.
NODE_ENV=production
PORT=3000
SENTRY_DSN=https://your-dsn@o123456.ingest.sentry.io/123456
SENTRY_TRACES_SAMPLE_RATE=1.0
SENTRY_PROFILES_SAMPLE_RATE=1.0
Create main application file
Build Express application with Sentry integration, error handling middleware, and sample routes for testing error tracking.
const Sentry = require('@sentry/node');
const { nodeProfilingIntegration } = require('@sentry/profiling-node');
const express = require('express');
const helmet = require('helmet');
const cors = require('cors');
require('dotenv').config();
// Initialize Sentry BEFORE requiring any other modules
Sentry.init({
dsn: process.env.SENTRY_DSN,
environment: process.env.NODE_ENV,
integrations: [
// enable HTTP calls tracing
new Sentry.Integrations.Http({ tracing: true }),
// enable Express.js middleware tracing
new Sentry.Integrations.Express({ app: express() }),
// enable profiling
nodeProfilingIntegration(),
],
// Performance monitoring
tracesSampleRate: parseFloat(process.env.SENTRY_TRACES_SAMPLE_RATE),
// Profiling sample rate
profilesSampleRate: parseFloat(process.env.SENTRY_PROFILES_SAMPLE_RATE),
});
const app = express();
const PORT = process.env.PORT || 3000;
// Sentry request handler must be first middleware
app.use(Sentry.Handlers.requestHandler());
app.use(Sentry.Handlers.tracingHandler());
// Security middleware
app.use(helmet());
app.use(cors());
app.use(express.json({ limit: '10mb' }));
app.use(express.urlencoded({ extended: true }));
// Health check endpoint
app.get('/health', (req, res) => {
res.status(200).json({
status: 'healthy',
timestamp: new Date().toISOString(),
uptime: process.uptime()
});
});
// Sample route that works
app.get('/', (req, res) => {
res.json({ message: 'Node.js app with Sentry error tracking' });
});
// Test route for throwing errors
app.get('/debug-sentry', (req, res) => {
throw new Error('Test error for Sentry - this should be caught!');
});
// Async error test
app.get('/async-error', async (req, res) => {
try {
await new Promise((resolve, reject) => {
setTimeout(() => reject(new Error('Async operation failed')), 100);
});
} catch (error) {
Sentry.captureException(error);
res.status(500).json({ error: 'Async error occurred' });
}
});
// Custom error with context
app.get('/context-error', (req, res) => {
Sentry.withScope((scope) => {
scope.setTag('section', 'api');
scope.setLevel('error');
scope.setContext('request_details', {
path: req.path,
method: req.method,
query: req.query
});
Sentry.captureException(new Error('Error with custom context'));
});
res.status(500).json({ error: 'Context error sent to Sentry' });
});
// 404 handler
app.use('*', (req, res) => {
res.status(404).json({ error: 'Route not found' });
});
// Sentry error handler must be before any other error middleware
app.use(Sentry.Handlers.errorHandler());
// Global error handler
app.use((err, req, res, next) => {
console.error('Unhandled error:', err.message);
res.status(500).json({
error: 'Internal server error',
requestId: res.sentry
});
});
// Graceful shutdown handling
process.on('SIGTERM', () => {
console.log('SIGTERM received, shutting down gracefully');
process.exit(0);
});
process.on('SIGINT', () => {
console.log('SIGINT received, shutting down gracefully');
process.exit(0);
});
// Unhandled promise rejection
process.on('unhandledRejection', (reason, promise) => {
console.error('Unhandled Rejection at:', promise, 'reason:', reason);
Sentry.captureException(new Error(Unhandled Rejection: ${reason}));
});
app.listen(PORT, () => {
console.log(Server running on port ${PORT});
console.log(Environment: ${process.env.NODE_ENV});
console.log(Sentry DSN configured: ${process.env.SENTRY_DSN ? 'Yes' : 'No'});
});
Create PM2 ecosystem configuration
Configure PM2 with clustering, monitoring, and automatic restart policies for production deployment.
module.exports = {
apps: [{
name: 'nodeapp-sentry',
script: './app.js',
instances: 'max', // Use all CPU cores
exec_mode: 'cluster',
env: {
NODE_ENV: 'development',
PORT: 3000
},
env_production: {
NODE_ENV: 'production',
PORT: 3000
},
// PM2 monitoring and restart policies
max_memory_restart: '1G',
min_uptime: '10s',
max_restarts: 10,
restart_delay: 4000,
// Logging configuration
log_file: '/var/log/nodeapp/combined.log',
out_file: '/var/log/nodeapp/out.log',
error_file: '/var/log/nodeapp/error.log',
log_date_format: 'YYYY-MM-DD HH:mm:ss Z',
// Advanced PM2 features
watch: false, // Set to true in development
ignore_watch: ['node_modules', 'logs'],
watch_options: {
followSymlinks: false
},
// Health monitoring
health_check_grace_period: 3000,
health_check_fatal_exceptions: true
}]
};
Create log directories and set permissions
Set up logging directories with correct ownership for PM2 and the application user.
sudo mkdir -p /var/log/nodeapp
sudo chown $USER:$USER /var/log/nodeapp
sudo chmod 755 /var/log/nodeapp
Set application ownership and permissions
Configure proper file ownership and permissions for the Node.js application directory.
sudo chown -R $USER:$USER /opt/nodeapp
sudo chmod -R 755 /opt/nodeapp
sudo chmod 600 /opt/nodeapp/.env
Start application with PM2
Launch the application using PM2 ecosystem configuration and enable startup script for automatic restart on server reboot.
cd /opt/nodeapp
pm2 start ecosystem.config.js --env production
pm2 save
pm2 startup
Configure firewall access
Open the application port in the firewall to allow external access while maintaining security.
sudo ufw allow 3000/tcp
sudo ufw reload
Configure Sentry alerts and notifications
Set up email and Slack notifications in your Sentry project dashboard. Navigate to Settings > Alerts and create rules for error frequency, new issues, and performance degradation.
Verify your setup
Test the Node.js application with Sentry integration and verify error tracking is working correctly.
# Check PM2 status
pm2 status
pm2 logs nodeapp-sentry --lines 20
Test application endpoints
curl http://localhost:3000/health
curl http://localhost:3000/
Test Sentry error tracking
curl http://localhost:3000/debug-sentry
curl http://localhost:3000/async-error
curl http://localhost:3000/context-error
Check PM2 monitoring
pm2 monit
Visit your Sentry dashboard to confirm errors are being captured with full stack traces, user context, and performance data. You should see the test errors appear within seconds.
Production configuration and best practices
Configure log rotation
Set up log rotation to prevent disk space issues in production environments.
/var/log/nodeapp/*.log {
daily
missingok
rotate 14
compress
delaycompress
notifempty
create 644 $USER $USER
postrotate
pm2 reloadLogs
endscript
}
Configure Sentry performance monitoring
Optimize Sentry configuration for production workloads with appropriate sampling rates.
# Production Sentry configuration
SENTRY_TRACES_SAMPLE_RATE=0.1
SENTRY_PROFILES_SAMPLE_RATE=0.1
SENTRY_RELEASE=v1.0.0
SENTRY_ENVIRONMENT=production
Set up reverse proxy with NGINX
Configure NGINX reverse proxy for better performance, SSL termination, and load balancing across PM2 cluster instances.
upstream nodeapp {
least_conn;
server 127.0.0.1:3000 max_fails=3 fail_timeout=30s;
keepalive 32;
}
server {
listen 80;
server_name example.com;
# Security headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
# Gzip compression
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_types text/plain text/css application/json application/javascript;
location / {
proxy_pass http://nodeapp;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache_bypass $http_upgrade;
# Timeouts
proxy_connect_timeout 30s;
proxy_send_timeout 30s;
proxy_read_timeout 30s;
}
# Health check endpoint
location /health {
proxy_pass http://nodeapp;
access_log off;
}
}
Advanced Sentry features
| Feature | Configuration | Use Case |
|---|---|---|
| Custom Tags | Sentry.setTag('feature', 'payment') |
Filter errors by application feature |
| User Context | Sentry.setUser({id: '123', email: 'user@example.com'}) |
Track errors affecting specific users |
| Breadcrumbs | Sentry.addBreadcrumb({message: 'API call started'}) |
Track user actions leading to errors |
| Performance | const transaction = Sentry.startTransaction({name: 'API'}) |
Monitor slow database queries and API calls |
| Release Tracking | SENTRY_RELEASE=v1.2.3 |
Track errors by deployment version |
Monitoring and alerting integration
For comprehensive production monitoring, integrate Sentry with your existing observability stack. You can connect Sentry error tracking with Prometheus metrics collection for complete application visibility.
Configure PM2 monitoring and log management with the PM2 advanced monitoring setup to complement Sentry's error tracking with system-level metrics.
Common issues
| Symptom | Cause | Fix |
|---|---|---|
| Errors not appearing in Sentry | Invalid DSN or network issues | Verify DSN in environment variables and check curl -I https://sentry.io |
| PM2 app crashing repeatedly | Memory limit exceeded or unhandled errors | Check pm2 logs and increase max_memory_restart in ecosystem config |
| High Sentry quota usage | Too many captured errors or high sampling rate | Reduce SENTRY_TRACES_SAMPLE_RATE to 0.1 and filter noisy errors |
| Missing error context | Sentry not configured before Express middleware | Ensure Sentry.init() is called before requiring Express |
| PM2 startup script not working | Incorrect user permissions | Run pm2 unstartup then pm2 startup and follow instructions |
Next steps
- Set up centralized logging with Winston and Elasticsearch
- Configure Node.js SSL certificates and HTTPS security
- Setup NGINX SSL certificates with PM2 clustering
- Configure Node.js JWT authentication with Redis session storage
- Implement Node.js application deployment with Git hooks
Running this in production?
Automated install script
Run this to automate the entire setup
#!/usr/bin/env bash
set -euo pipefail
# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
# Default values
APP_DIR="/opt/nodeapp"
APP_USER="nodeapp"
PORT="3000"
SENTRY_DSN=""
# Usage function
usage() {
echo "Usage: $0 --sentry-dsn=<DSN> [--port=PORT] [--app-dir=DIR]"
echo "Example: $0 --sentry-dsn=https://abc123@o123456.ingest.sentry.io/123456"
exit 1
}
# Parse arguments
for arg in "$@"; do
case $arg in
--sentry-dsn=*)
SENTRY_DSN="${arg#*=}"
;;
--port=*)
PORT="${arg#*=}"
;;
--app-dir=*)
APP_DIR="${arg#*=}"
;;
--help|-h)
usage
;;
*)
echo -e "${RED}Unknown argument: $arg${NC}"
usage
;;
esac
done
# Validate required arguments
if [[ -z "$SENTRY_DSN" ]]; then
echo -e "${RED}Error: --sentry-dsn is required${NC}"
usage
fi
# Cleanup function
cleanup() {
echo -e "${RED}Installation failed. Cleaning up...${NC}"
if id "$APP_USER" &>/dev/null; then
userdel -r "$APP_USER" 2>/dev/null || true
fi
rm -rf "$APP_DIR" 2>/dev/null || true
}
trap cleanup ERR
# Check if running as root
if [[ $EUID -ne 0 ]]; then
echo -e "${RED}This script must be run as root${NC}"
exit 1
fi
# Auto-detect distribution
echo -e "${YELLOW}[1/12] Detecting distribution...${NC}"
if [ -f /etc/os-release ]; then
. /etc/os-release
case "$ID" in
ubuntu|debian)
PKG_MGR="apt"
PKG_UPDATE="apt update && apt upgrade -y"
PKG_INSTALL="apt install -y"
BUILD_TOOLS="curl build-essential"
;;
almalinux|rocky|centos|rhel|ol|fedora)
PKG_MGR="dnf"
PKG_UPDATE="dnf update -y"
PKG_INSTALL="dnf install -y"
BUILD_TOOLS="curl"
# Install development tools group
dnf groupinstall -y "Development Tools" 2>/dev/null || yum groupinstall -y "Development Tools" 2>/dev/null || true
;;
amzn)
PKG_MGR="yum"
PKG_UPDATE="yum update -y"
PKG_INSTALL="yum install -y"
BUILD_TOOLS="curl"
yum groupinstall -y "Development Tools" 2>/dev/null || true
;;
*)
echo -e "${RED}Unsupported distribution: $ID${NC}"
exit 1
;;
esac
else
echo -e "${RED}Cannot detect distribution${NC}"
exit 1
fi
echo -e "${GREEN}Detected: $PRETTY_NAME${NC}"
# Update system packages
echo -e "${YELLOW}[2/12] Updating system packages...${NC}"
$PKG_UPDATE
$PKG_INSTALL $BUILD_TOOLS
# Install Node.js LTS
echo -e "${YELLOW}[3/12] Installing Node.js LTS...${NC}"
if [[ "$PKG_MGR" == "apt" ]]; then
curl -fsSL https://deb.nodesource.com/setup_lts.x | bash -
$PKG_INSTALL nodejs
else
curl -fsSL https://rpm.nodesource.com/setup_lts.x | bash -
$PKG_INSTALL nodejs npm
fi
# Verify Node.js installation
node_version=$(node --version)
npm_version=$(npm --version)
echo -e "${GREEN}Node.js $node_version and npm $npm_version installed${NC}"
# Install PM2 globally
echo -e "${YELLOW}[4/12] Installing PM2 process manager...${NC}"
npm install -g pm2
pm2_version=$(pm2 --version)
echo -e "${GREEN}PM2 $pm2_version installed${NC}"
# Create application user
echo -e "${YELLOW}[5/12] Creating application user...${NC}"
if ! id "$APP_USER" &>/dev/null; then
useradd -r -s /bin/false -d "$APP_DIR" "$APP_USER"
fi
# Create application directory
echo -e "${YELLOW}[6/12] Creating application directory...${NC}"
mkdir -p "$APP_DIR"
chown "$APP_USER:$APP_USER" "$APP_DIR"
chmod 755 "$APP_DIR"
# Initialize Node.js project
echo -e "${YELLOW}[7/12] Initializing Node.js project...${NC}"
cd "$APP_DIR"
sudo -u "$APP_USER" npm init -y
# Install dependencies
echo -e "${YELLOW}[8/12] Installing Node.js dependencies...${NC}"
sudo -u "$APP_USER" npm install @sentry/node @sentry/profiling-node express helmet cors dotenv
sudo -u "$APP_USER" npm install --save-dev nodemon
# Create environment file
echo -e "${YELLOW}[9/12] Creating environment configuration...${NC}"
cat > "$APP_DIR/.env" << EOF
NODE_ENV=production
PORT=$PORT
SENTRY_DSN=$SENTRY_DSN
SENTRY_TRACES_SAMPLE_RATE=1.0
SENTRY_PROFILES_SAMPLE_RATE=1.0
EOF
chown "$APP_USER:$APP_USER" "$APP_DIR/.env"
chmod 600 "$APP_DIR/.env"
# Create main application file
echo -e "${YELLOW}[10/12] Creating main application file...${NC}"
cat > "$APP_DIR/server.js" << 'EOF'
const Sentry = require('@sentry/node');
const { nodeProfilingIntegration } = require('@sentry/profiling-node');
const express = require('express');
const helmet = require('helmet');
const cors = require('cors');
require('dotenv').config();
Sentry.init({
dsn: process.env.SENTRY_DSN,
environment: process.env.NODE_ENV,
integrations: [
new Sentry.Integrations.Http({ tracing: true }),
new Sentry.Integrations.Express({ app: express() }),
nodeProfilingIntegration(),
],
tracesSampleRate: parseFloat(process.env.SENTRY_TRACES_SAMPLE_RATE),
profilesSampleRate: parseFloat(process.env.SENTRY_PROFILES_SAMPLE_RATE),
});
const app = express();
const PORT = process.env.PORT || 3000;
app.use(Sentry.Handlers.requestHandler());
app.use(Sentry.Handlers.tracingHandler());
app.use(helmet());
app.use(cors());
app.use(express.json({ limit: '10mb' }));
app.use(express.urlencoded({ extended: true }));
app.get('/health', (req, res) => {
res.status(200).json({
status: 'healthy',
timestamp: new Date().toISOString(),
uptime: process.uptime()
});
});
app.get('/', (req, res) => {
res.json({ message: 'Node.js app with Sentry monitoring' });
});
app.get('/error', (req, res, next) => {
const error = new Error('Test error for Sentry');
next(error);
});
app.use(Sentry.Handlers.errorHandler());
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(500).json({ error: 'Something went wrong!' });
});
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
EOF
chown "$APP_USER:$APP_USER" "$APP_DIR/server.js"
chmod 644 "$APP_DIR/server.js"
# Create PM2 ecosystem file
echo -e "${YELLOW}[11/12] Creating PM2 configuration...${NC}"
cat > "$APP_DIR/ecosystem.config.js" << EOF
module.exports = {
apps: [{
name: 'nodeapp-sentry',
script: './server.js',
instances: 'max',
exec_mode: 'cluster',
env: {
NODE_ENV: 'production',
PORT: $PORT
},
error_file: './logs/err.log',
out_file: './logs/out.log',
log_file: './logs/combined.log',
time: true,
max_memory_restart: '1G'
}]
};
EOF
chown "$APP_USER:$APP_USER" "$APP_DIR/ecosystem.config.js"
chmod 644 "$APP_DIR/ecosystem.config.js"
# Create logs directory
mkdir -p "$APP_DIR/logs"
chown "$APP_USER:$APP_USER" "$APP_DIR/logs"
chmod 755 "$APP_DIR/logs"
# Start application with PM2
echo -e "${YELLOW}[12/12] Starting application...${NC}"
sudo -u "$APP_USER" PM2_HOME="$APP_DIR/.pm2" pm2 start "$APP_DIR/ecosystem.config.js"
# Configure PM2 startup
sudo env PATH=$PATH:/usr/bin PM2_HOME="$APP_DIR/.pm2" pm2 startup systemd -u "$APP_USER" --hp "$APP_DIR" --service-name pm2-nodeapp
# Save PM2 configuration
sudo -u "$APP_USER" PM2_HOME="$APP_DIR/.pm2" pm2 save
echo -e "${GREEN}Installation completed successfully!${NC}"
echo -e "${GREEN}Application is running on http://localhost:$PORT${NC}"
echo -e "${GREEN}Health check: http://localhost:$PORT/health${NC}"
echo -e "${GREEN}Test error tracking: http://localhost:$PORT/error${NC}"
echo ""
echo -e "${YELLOW}PM2 Commands:${NC}"
echo " View status: sudo -u $APP_USER PM2_HOME=$APP_DIR/.pm2 pm2 status"
echo " View logs: sudo -u $APP_USER PM2_HOME=$APP_DIR/.pm2 pm2 logs"
echo " Restart: sudo -u $APP_USER PM2_HOME=$APP_DIR/.pm2 pm2 restart nodeapp-sentry"
Review the script before running. Execute with: bash install.sh