Skip to content

Commit 7c090f4

Browse files
committed
Merge PR #239: feat/sentry-integration
2 parents 0786f42 + 8d5dc8e commit 7c090f4

14 files changed

Lines changed: 3658 additions & 1414 deletions

backend/.env.example

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,12 @@ RATE_LIMIT_ADMIN_MAX=100
6060
RATE_LIMIT_ADMIN_WINDOW_HOURS=1
6161

6262
# External API Keys (optional)
63+
ANTHROPIC_API_KEY=your_anthropic_api_key_for_classification
64+
# Sentry
65+
SENTRY_DSN=your_sentry_dsn_here
66+
SENTRY_AUTH_TOKEN=your_sentry_auth_token_here
67+
SENTRY_ORG=your_sentry_org_here
68+
SENTRY_PROJECT=your_sentry_project_here
6369
ANTHROPIC_API_KEY=your_anthropic_api_key_for_classification
6470
# Risk calculation concurrency (number of simultaneous risk calculations per page)
6571
RISK_CALC_CONCURRENCY=10

backend/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
"test": "jest"
1515
},
1616
"dependencies": {
17+
"@sentry/node": "^10.46.0",
18+
"@sentry/profiling-node": "^10.46.0",
1719
"@stellar/stellar-sdk": "^14.5.0",
1820
"@supabase/supabase-js": "^2.47.10",
1921
"@types/cookie-parser": "^1.4.10",

backend/src/index.ts

Lines changed: 210 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,219 @@
1-
import * as bip39 from 'bip39';
1+
import express from 'express';
2+
import cookieParser from 'cookie-parser';
3+
import dotenv from 'dotenv';
4+
import * as Sentry from '@sentry/node';
5+
import { nodeProfilingIntegration } from '@sentry/profiling-node';
6+
7+
// Load environment variables before importing other modules
8+
dotenv.config();
9+
10+
Sentry.init({
11+
dsn: process.env.SENTRY_DSN,
12+
environment: process.env.NODE_ENV,
13+
integrations: [nodeProfilingIntegration()],
14+
tracesSampleRate: 0.1,
15+
profilesSampleRate: 0.1,
16+
});
17+
18+
19+
import logger from './config/logger';
20+
import { requestIdMiddleware } from './middleware/requestContext';
21+
import { requestLoggerMiddleware } from './middleware/requestLogger';
22+
import { schedulerService } from './services/scheduler';
23+
import { reminderEngine } from './services/reminder-engine';
24+
import subscriptionRoutes from './routes/subscriptions';
25+
import riskScoreRoutes from './routes/risk-score';
26+
import simulationRoutes from './routes/simulation';
27+
import merchantRoutes from './routes/merchants';
28+
import teamRoutes from './routes/team';
29+
import auditRoutes from './routes/audit';
30+
import webhookRoutes from './routes/webhooks';
31+
import { monitoringService } from './services/monitoring-service';
32+
import { healthService } from './services/health-service';
33+
import { eventListener } from './services/event-listener';
34+
import { expiryService } from './services/expiry-service';
35+
import { scheduleAutoResume } from './jobs/auto-resume';
36+
37+
const app = express();
38+
39+
// Add Sentry request handler before routes
40+
app.use(Sentry.Handlers.requestHandler());
41+
42+
const PORT = process.env.PORT || 3001;
43+
const ADMIN_API_KEY = process.env.ADMIN_API_KEY || 'development-admin-key';
44+
45+
// CORS configuration
46+
const FRONTEND_URL = process.env.FRONTEND_URL || 'http://localhost:3000';
47+
app.use((req, res, next) => {
48+
res.header('Access-Control-Allow-Origin', FRONTEND_URL);
49+
res.header('Access-Control-Allow-Credentials', 'true');
50+
res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, PATCH, DELETE, OPTIONS');
51+
res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization, Idempotency-Key, If-Match');
52+
53+
if (req.method === 'OPTIONS') {
54+
return res.sendStatus(200);
55+
}
56+
next();
57+
});
258

59+
// Middleware
60+
app.use(cookieParser());
61+
app.use(express.json());
62+
app.use(express.urlencoded({ extended: true }));
63+
64+
// Request tracing — must come before routes so every log line carries requestId
65+
app.use(requestIdMiddleware);
66+
app.use(requestLoggerMiddleware);
67+
68+
69+
import { adminAuth } from './middleware/admin';
70+
import { createAdminLimiter, RateLimiterFactory } from './middleware/rate-limit-factory';
71+
72+
// Health check endpoint
73+
app.get('/health', (req, res) => {
74+
res.json({ status: 'ok', timestamp: new Date().toISOString() });
75+
});
76+
77+
// API Routes
78+
app.use('/api/subscriptions', subscriptionRoutes);
79+
app.use('/api/risk-score', riskScoreRoutes);
80+
app.use('/api/simulation', simulationRoutes);
81+
app.use('/api/merchants', merchantRoutes);
82+
app.use('/api/team', teamRoutes);
83+
app.use('/api/audit', auditRoutes);
84+
app.use('/api/webhooks', webhookRoutes);
85+
86+
// API Routes (Public/Standard)
87+
app.get('/api/reminders/status', (req, res) => {
88+
const status = schedulerService.getStatus();
89+
res.json(status);
90+
});
91+
92+
// Admin Monitoring Endpoints (Read-only)
93+
app.get('/api/admin/metrics/subscriptions', createAdminLimiter(), adminAuth, async (req, res) => {
94+
try {
95+
const metrics = await monitoringService.getSubscriptionMetrics();
96+
res.json(metrics);
97+
} catch (error) {
98+
res.status(500).json({ error: 'Failed to fetch subscription metrics' });
99+
}
100+
});
101+
102+
app.get('/api/admin/metrics/renewals', createAdminLimiter(), adminAuth, async (req, res) => {
103+
try {
104+
const metrics = await monitoringService.getRenewalMetrics();
105+
res.json(metrics);
106+
} catch (error) {
107+
res.status(500).json({ error: 'Failed to fetch renewal metrics' });
108+
}
109+
});
110+
111+
app.get('/api/admin/metrics/activity', createAdminLimiter(), adminAuth, async (req, res) => {
112+
try {
113+
const metrics = await monitoringService.getAgentActivity();
114+
res.json(metrics);
115+
} catch (error) {
116+
res.status(500).json({ error: 'Failed to fetch agent activity' });
117+
}
118+
});
119+
120+
// Protocol Health Monitor: unified admin health (metrics, alerts, history)
121+
app.get('/api/admin/health', createAdminLimiter(), adminAuth, async (req, res) => {
122+
try {
123+
const includeHistory = req.query.history !== 'false';
124+
const health = await healthService.getAdminHealth(includeHistory);
125+
const statusCode = health.status === 'unhealthy' ? 503 : 200;
126+
res.status(statusCode).json(health);
127+
} catch (error) {
128+
logger.error('Error fetching admin health:', error);
129+
res.status(500).json({ error: 'Failed to fetch health status' });
130+
}
131+
});
132+
133+
// Manual trigger endpoints (for testing/admin - Should eventually be protected)
134+
app.post('/api/reminders/process', createAdminLimiter(), adminAuth, async (req, res) => {
135+
try {
136+
await reminderEngine.processReminders();
137+
res.json({ success: true, message: 'Reminders processed' });
138+
} catch (error) {
139+
logger.error('Error processing reminders:', error);
140+
res.status(500).json({
141+
success: false,
142+
error: error instanceof Error ? error.message : String(error),
143+
});
144+
}
145+
});
146+
147+
app.post('/api/reminders/schedule', createAdminLimiter(), adminAuth, async (req, res) => {
148+
try {
149+
const daysBefore = req.body.daysBefore || [7, 3, 1];
150+
await reminderEngine.scheduleReminders(daysBefore);
151+
res.json({ success: true, message: 'Reminders scheduled' });
152+
} catch (error) {
153+
logger.error('Error scheduling reminders:', error);
154+
res.status(500).json({
155+
success: false,
156+
error: error instanceof Error ? error.message : String(error),
157+
});
158+
}
159+
});
160+
161+
app.post('/api/reminders/retry', createAdminLimiter(), adminAuth, async (req, res) => {
162+
try {
163+
await reminderEngine.processRetries();
164+
res.json({ success: true, message: 'Retries processed' });
165+
} catch (error) {
166+
logger.error('Error processing retries:', error);
167+
res.status(500).json({
168+
success: false,
169+
error: error instanceof Error ? error.message : String(error),
170+
});
171+
}
172+
});
173+
174+
// Protocol Health Monitor: record metrics snapshot periodically (historical storage)
175+
const HEALTH_SNAPSHOT_INTERVAL_MS = 15 * 60 * 1000; // 15 minutes
176+
function startHealthSnapshotInterval() {
177+
setInterval(() => {
178+
healthService.recordSnapshot().catch(() => {});
179+
}, HEALTH_SNAPSHOT_INTERVAL_MS);
180+
// Record one snapshot shortly after startup
181+
setTimeout(() => healthService.recordSnapshot().catch(() => {}), 5000);
182+
}
183+
184+
app.post('/api/admin/expiry/process', createAdminLimiter(), adminAuth, async (req, res) => {
185+
try {
186+
const result = await expiryService.processExpiries();
187+
res.json({ success: true, data: result });
188+
} catch (error) {
189+
logger.error('Error processing expiries:', error);
190+
res.status(500).json({
191+
success: false,
192+
error: error instanceof Error ? error.message : String(error),
193+
});
194+
}
195+
});
196+
197+
// Add Sentry error handler after all routes
198+
app.use(Sentry.Handlers.errorHandler());
199+
200+
// Start server
201+
const server = app.listen(PORT, async () => {
202+
logger.info(`Server running on port ${PORT}`);
203+
logger.info(`Environment: ${process.env.NODE_ENV || 'development'}`);
204+
205+
// Initialize rate limiting Redis store
206+
try {
207+
await RateLimiterFactory.initializeRedisStore();
208+
logger.info('Rate limiting initialized successfully');
209+
} catch (error) {
210+
logger.warn('Rate limiting initialization failed, using memory store:', error);
211+
import * as bip39 from 'bip39';
3212
/**
4213
* Generates a standard BIP39 12-word mnemonic phrase.
5214
*/
6215
export function generateMnemonic(): string {
7216
return bip39.generateMnemonic(128);
8-
}
9-
10217
/**
11218
* Validates a 12-word BIP39 mnemonic phrase.
12219
*/

backend/src/middleware/auth.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { Request, Response, NextFunction } from 'express';
22
import { supabase } from '../config/database';
33
import logger from '../config/logger';
44
import { setRequestUserId } from './requestContext';
5+
import * as Sentry from '@sentry/node';
56

67
export interface AuthenticatedRequest extends Request {
78
user?: {
@@ -57,6 +58,8 @@ export async function authenticate(
5758
email: user.email || '',
5859
};
5960
setRequestUserId(user.id);
61+
Sentry.setUser({ id: user.id, email: user.email });
62+
6063

6164
next();
6265
} catch (error) {
@@ -95,7 +98,9 @@ export async function optionalAuthenticate(
9598
email: user.email || '',
9699
};
97100
setRequestUserId(user.id);
101+
Sentry.setUser({ id: user.id, email: user.email });
98102
}
103+
99104
}
100105

101106
next();

0 commit comments

Comments
 (0)