Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
929 changes: 922 additions & 7 deletions backend/package-lock.json

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
"license": "ISC",
"description": "",
"dependencies": {
"@sentry/node": "^10.25.0",
"@sentry/profiling-node": "^10.25.0",
"@types/express-validator": "^2.20.33",
"@types/socket.io": "^3.0.1",
"bcryptjs": "^3.0.2",
Expand Down
17 changes: 17 additions & 0 deletions backend/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import express from "express";
import cookieParser from 'cookie-parser';
import { createServer } from "http";
import cors from "cors";
import * as Sentry from '@sentry/node';
import logger from "./utils/logger";
import {
corsConfig,
Expand All @@ -15,6 +16,12 @@ import {
} from "./middleware/security";
import { csrfProtection, csrfErrorHandler } from "./middleware/csrf";
import { requestLogger, errorLogger, notFoundLogger, performanceLogger } from "./middleware/logging";
import {
sentryUserContext,
sentryRequestContext,
sentryErrorHandler,
sentryPerformanceMonitoring
} from "./middleware/sentry";
import healthRoutes from "./routes/health";
import authRoutes from "./routes/auth";
import projectRoutes from "./routes/project";
Expand All @@ -23,6 +30,7 @@ import pullRequestRoutes from "./routes/pullRequest";
import notificationRoutes from "./routes/notification";
import userRoutes from "./routes/user";
import branchProtectionRoutes from "./routes/branchProtection";
import testSentryRoutes from "./routes/test-sentry";
import SocketService from "./services/SocketService";

export function createApp() {
Expand All @@ -35,6 +43,7 @@ export function createApp() {
// Logging Middleware (early in the chain)
app.use(requestLogger);
app.use(performanceLogger(1000)); // Log requests taking > 1 second
app.use(sentryPerformanceMonitoring(3000)); // Track slow requests > 3s for Sentry

// Security Middleware (order matters!)
app.use(helmetConfig); // Security headers
Expand All @@ -54,6 +63,10 @@ export function createApp() {
// Trust proxy for accurate IP addresses (important for rate limiting)
app.set('trust proxy', 1);

// Sentry context middleware (after body parsing, before routes)
app.use(sentryRequestContext);
app.use(sentryUserContext); // Will add user context if authenticated

// Routes
app.get("/", (req, res) => {
res.json({ message: "Welcome to Collab Code Review API", status: "running" });
Expand All @@ -73,10 +86,14 @@ export function createApp() {
app.use("/api/notifications", generalLimiter, notificationRoutes);
app.use("/api/users", generalLimiter, userRoutes);
app.use("/api/branch-protection", branchProtectionRoutes);
app.use("/api/test-sentry", testSentryRoutes); // Test Sentry integration

// 404 handler
app.use(notFoundLogger);

// Sentry Error Handler - MUST be before other error handlers
app.use(sentryErrorHandler);

// Error logging middleware (before error handlers)
app.use(errorLogger);

Expand Down
18 changes: 18 additions & 0 deletions backend/src/config/db.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,36 @@
import mongoose from "mongoose";
import logger from "../utils/logger";
import { addSentryBreadcrumb, captureException } from "./sentry";

const connectDB = async () => {
try {
addSentryBreadcrumb("Attempting to connect to MongoDB", "database", "info");

await mongoose.connect(process.env.MONGO_URI as string);
logger.info("MongoDB connected", {
host: mongoose.connection.host,
database: mongoose.connection.name,
});

addSentryBreadcrumb("MongoDB connected successfully", "database", "info", {
host: mongoose.connection.host,
database: mongoose.connection.name,
});
} catch (error) {
logger.error("MongoDB connection failed", {
error: error instanceof Error ? error.message : String(error),
stack: error instanceof Error ? error.stack : undefined,
});

if (error instanceof Error) {
captureException(error, {
database: {
operation: "connect",
uri: process.env.MONGO_URI ? "configured" : "missing",
}
});
}

process.exit(1);
}
};
Expand Down
235 changes: 235 additions & 0 deletions backend/src/config/sentry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,235 @@
import * as Sentry from '@sentry/node';
import { nodeProfilingIntegration } from '@sentry/profiling-node';

/**
* Initialize Sentry for error tracking and performance monitoring
*
* Features:
* - Automatic error capture
* - Performance monitoring
* - Profiling for performance bottlenecks
* - User context tracking
* - Custom tags and metadata
* - Release tracking
*/
export const initializeSentry = () => {
const sentryDsn = process.env.SENTRY_DSN;
const environment = process.env.NODE_ENV || 'development';
const release = process.env.npm_package_version || 'unknown';

// Only initialize if DSN is provided
if (!sentryDsn) {
console.warn('SENTRY_DSN not configured. Sentry error tracking is disabled.');
return;
}

Sentry.init({
dsn: sentryDsn,
environment,
release: `collab-code-review@${release}`,

// Performance Monitoring
tracesSampleRate: environment === 'production' ? 0.1 : 1.0, // 10% in prod, 100% in dev

// Profiling
profilesSampleRate: environment === 'production' ? 0.1 : 1.0,

integrations: [
// Automatic instrumentation
nodeProfilingIntegration(),
],

// Send default PII (IP addresses, user context)
sendDefaultPii: true,

// Error filtering
beforeSend(event, hint) {
const error = hint.originalException;

// Don't send certain errors to Sentry
if (error && typeof error === 'object' && 'statusCode' in error) {
const statusCode = (error as any).statusCode;
// Skip client errors (4xx) except authentication issues
if (statusCode >= 400 && statusCode < 500 && statusCode !== 401 && statusCode !== 403) {
return null;
}
}

// Filter out known non-critical errors
if (event.message?.includes('ECONNRESET') || event.message?.includes('EPIPE')) {
return null;
}

return event;
},

// Breadcrumb filtering
beforeBreadcrumb(breadcrumb, hint) {
// Don't log sensitive data in breadcrumbs
if (breadcrumb.category === 'http' && breadcrumb.data) {
// Remove sensitive headers
if (breadcrumb.data.headers) {
delete breadcrumb.data.headers.authorization;
delete breadcrumb.data.headers.cookie;
}
// Remove sensitive query params
if (breadcrumb.data.query_string) {
breadcrumb.data.query_string = breadcrumb.data.query_string
.replace(/token=[^&]+/gi, 'token=[REDACTED]')
.replace(/password=[^&]+/gi, 'password=[REDACTED]');
}
}
return breadcrumb;
},

// Ignore certain errors
ignoreErrors: [
// Browser/client errors that shouldn't be tracked
'Non-Error promise rejection captured',
'ResizeObserver loop limit exceeded',
// Network errors
'NetworkError',
'Network request failed',
// Validation errors (these should be handled gracefully)
'ValidationError',
],

// Sample rate for error events (100% = capture all errors)
sampleRate: 1.0,

// Maximum breadcrumbs to keep
maxBreadcrumbs: 50,

// Attach stack traces to messages
attachStacktrace: true,

// Enable debug mode only when explicitly needed
debug: false,

// Server name
serverName: process.env.SERVER_NAME || 'collab-code-review-backend',
});

console.log(`Sentry initialized for ${environment} environment`);
};

/**
* Set user context for error tracking
*/
export const setSentryUser = (user: {
id: string;
email?: string;
username?: string;
role?: string;
}) => {
const userData: Record<string, string> = { id: user.id };
if (user.email) userData.email = user.email;
if (user.username) userData.username = user.username;
if (user.role) userData.role = user.role;

Sentry.setUser(userData);
};

/**
* Clear user context (e.g., on logout)
*/
export const clearSentryUser = () => {
Sentry.setUser(null);
};

/**
* Add custom context to error reports
*/
export const setSentryContext = (key: string, context: Record<string, any>) => {
Sentry.setContext(key, context);
};

/**
* Add tags for filtering in Sentry dashboard
*/
export const setSentryTag = (key: string, value: string) => {
Sentry.setTag(key, value);
};

/**
* Add breadcrumb for tracking user actions
*/
export const addSentryBreadcrumb = (
message: string,
category: string,
level: Sentry.SeverityLevel = 'info',
data?: Record<string, any>
) => {
const breadcrumb: Sentry.Breadcrumb = {
message,
category,
level,
timestamp: Date.now() / 1000,
};

if (data) {
breadcrumb.data = data;
}

Sentry.addBreadcrumb(breadcrumb);
};

/**
* Manually capture an exception
*/
export const captureException = (error: Error, context?: Record<string, any>) => {
if (context) {
Sentry.withScope((scope) => {
Object.entries(context).forEach(([key, value]) => {
scope.setContext(key, value);
});
Sentry.captureException(error);
});
} else {
Sentry.captureException(error);
}
};

/**
* Capture a message (for non-error events)
*/
export const captureMessage = (
message: string,
level: Sentry.SeverityLevel = 'info',
context?: Record<string, any>
) => {
if (context) {
Sentry.withScope((scope) => {
Object.entries(context).forEach(([key, value]) => {
scope.setContext(key, value);
});
Sentry.captureMessage(message, level);
});
} else {
Sentry.captureMessage(message, level);
}
};

/**
* Start a performance transaction
*/
export const startTransaction = (
name: string,
op: string,
data?: Record<string, any>
) => {
const options: any = { name, op };
if (data) {
options.attributes = data;
}
return Sentry.startSpan(options, (span) => span);
};

/**
* Get current scope for manual instrumentation
*/
export const getSentryScope = () => {
return Sentry.getCurrentScope();
};

export { Sentry };
12 changes: 10 additions & 2 deletions backend/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
import dotenv from "dotenv";
dotenv.config();

// Initialize Sentry FIRST
import { initializeSentry } from "./config/sentry";
initializeSentry();

import connectDB from "./config/db";
import { createApp } from "./app";
import logger from "./utils/logger";

dotenv.config();

connectDB();

const { app, server, socketService } = createApp();
Expand All @@ -26,6 +30,10 @@ server.listen(PORT, () => {
logger.info(`Server running on http://localhost:${PORT}`);
logger.info(`Socket.IO server ready for connections`);
logger.info(`Environment: ${process.env.NODE_ENV || 'development'}`);

//log to console in case Winston console transport is disabled
console.log(`✅ Server running on http://localhost:${PORT}`);
console.log(`✅ Environment: ${process.env.NODE_ENV || 'development'}`);
});

// Graceful shutdown
Expand Down
Loading
Loading