From 2e1285a225f6d1278694fe3385b4c7f66c58fe67 Mon Sep 17 00:00:00 2001 From: Truphile Date: Fri, 19 Jun 2026 07:58:23 -0400 Subject: [PATCH] feat: integrate Sentry for error tracking and fix metrics DI startup error --- .env.example | 52 +++++++++++++++++++++++++ README.md | 15 +++++++ context/progress-tracker.md | 8 ++++ src/app.module.ts | 8 +++- src/main.ts | 10 +++++ src/modules/health/health.controller.ts | 7 ++++ src/modules/metrics/metrics.module.ts | 5 ++- src/modules/metrics/metrics.service.ts | 15 +++---- 8 files changed, 110 insertions(+), 10 deletions(-) create mode 100644 .env.example diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..377ed0c --- /dev/null +++ b/.env.example @@ -0,0 +1,52 @@ +# Application +NODE_ENV=development +PORT=3000 +API_URL=http://localhost:3000 + +# Database +SUPABASE_URL=https://your-project.supabase.co +SUPABASE_ANON_KEY=your-anon-key +SUPABASE_SERVICE_KEY=your-service-role-key +SUPABASE_SERVICE_ROLE_KEY=your-service-role-key +DATABASE_URL=postgresql://postgres:password@db.your-project.supabase.co:5432/postgres + +# Stellar +STELLAR_NETWORK=testnet +STELLAR_HORIZON_URL=https://horizon-testnet.stellar.org +STELLAR_SOROBAN_URL=https://soroban-testnet.stellar.org +STELLAR_NETWORK_PASSPHRASE=Test SDF Network ; September 2015 + +# Smart Contract IDs (deployed smart contracts) +REPUTATION_CONTRACT_ID= +CREDITLINE_CONTRACT_ID= +MERCHANT_REGISTRY_CONTRACT_ID= +LIQUIDITY_POOL_CONTRACT_ID= + +# Auth +JWT_SECRET= +JWT_ACCESS_EXPIRATION=15m +JWT_REFRESH_EXPIRATION=7d +NONCE_EXPIRATION=300 + +# Redis +REDIS_URL=redis://localhost:6379 +REDIS_DB=0 +REPUTATION_CACHE_TTL=300 + +# Logging +LOG_LEVEL=debug +LOG_PRETTY=true + +# CORS +CORS_ORIGINS=http://localhost:3000,http://localhost:5173 +CORS_CREDENTIALS=true + +# Jobs +JOBS_ENABLED=true +INDEXER_INTERVAL=30 +TX_STATUS_INTERVAL=15 +REMINDER_CRON=0 9 * * * + +# Sentry +SENTRY_DSN= +SENTRY_TRACES_SAMPLE_RATE=0.1 diff --git a/README.md b/README.md index 7dd40a4..0d582e0 100644 --- a/README.md +++ b/README.md @@ -165,6 +165,21 @@ The bootstrap command will guide you through the process of setting up a Supabas | **Swagger Docs** | https://stepfi-api.onrender.com/api/v1/docs | | **Health Check** | https://stepfi-api.onrender.com/api/v1/health | +## 🛠️ Error tracking + +Error tracking is powered by Sentry. To enable error tracking in development or production: + +1. Create a Sentry project for Nest.js. +2. Add the following environment variables to your `.env` file: + ```env + # Sentry DSN for error reporting + SENTRY_DSN=https://your-sentry-dsn@sentry.io/project-id + + # Optional: Sentry Traces Sample Rate (defaults to 0.1) + SENTRY_TRACES_SAMPLE_RATE=0.1 + ``` +3. In production, unhandled exceptions will automatically be reported to Sentry. If `SENTRY_DSN` is not provided, the Sentry SDK will operate in no-op mode (silently) and the app will log unhandled exceptions only to stdout. + ## Docs Command & config reference can be found [here](https://supabase.com/docs/reference/cli/about). diff --git a/context/progress-tracker.md b/context/progress-tracker.md index acebec8..c5fd01f 100644 --- a/context/progress-tracker.md +++ b/context/progress-tracker.md @@ -50,6 +50,14 @@ EAS build for Expo preview → then landing page → then GitHub issues → then - `node_modules` cached via `actions/cache@v4` keyed on `package-lock.json` hash - CI status badge added to `README.md` pointing at the workflow +### Error Tracking +- Integrated `@sentry/nestjs` into the API orchestrator. +- Configured Sentry initialization at the very top of `src/main.ts` before NestJS bootstrap. +- Registered `SentryModule` and global exception filter `SentryGlobalFilter` in `src/app.module.ts`. +- Created `.env.example` with template environment variables, including `SENTRY_DSN` and `SENTRY_TRACES_SAMPLE_RATE`. +- Added `sentry-test` endpoint to `HealthController` for verification. +- Documented Sentry configuration in `README.md`. + --- ## In Progress diff --git a/src/app.module.ts b/src/app.module.ts index d53fd38..8286ee7 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -1,8 +1,9 @@ import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common'; -import { APP_GUARD } from '@nestjs/core'; +import { APP_GUARD, APP_FILTER } from '@nestjs/core'; import { ConfigModule, ConfigService } from '@nestjs/config'; import { ThrottlerModule, ThrottlerGuard } from '@nestjs/throttler'; import { BullModule } from '@nestjs/bullmq'; +import { SentryModule, SentryGlobalFilter } from '@sentry/nestjs/setup'; import { AuthModule } from './modules/auth/auth.module'; import { HealthModule } from './modules/health/health.module'; import { LoansModule } from './modules/loans/loans.module'; @@ -29,6 +30,7 @@ import { CorrelationIdMiddleware } from './common/logger/correlation-id.middlewa @Module({ imports: [ ConfigModule.forRoot({ isGlobal: true }), + SentryModule.forRoot(), LoggerModule, ThrottlerModule.forRoot([ { @@ -72,6 +74,10 @@ import { CorrelationIdMiddleware } from './common/logger/correlation-id.middlewa provide: APP_GUARD, useClass: ThrottlerGuard, }, + { + provide: APP_FILTER, + useClass: SentryGlobalFilter, + }, ], }) export class AppModule implements NestModule { diff --git a/src/main.ts b/src/main.ts index d26f5cf..1f8d86d 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,3 +1,13 @@ +import * as Sentry from '@sentry/nestjs'; + +Sentry.init({ + dsn: process.env.SENTRY_DSN || undefined, + environment: process.env.NODE_ENV || 'development', + tracesSampleRate: process.env.SENTRY_TRACES_SAMPLE_RATE + ? parseFloat(process.env.SENTRY_TRACES_SAMPLE_RATE) + : 0.1, +}); + import { ValidationPipe } from '@nestjs/common'; import { NestFactory } from '@nestjs/core'; import { FastifyAdapter, NestFastifyApplication } from '@nestjs/platform-fastify'; diff --git a/src/modules/health/health.controller.ts b/src/modules/health/health.controller.ts index a40243c..ae34ec1 100644 --- a/src/modules/health/health.controller.ts +++ b/src/modules/health/health.controller.ts @@ -23,4 +23,11 @@ export class HealthController { async checkDatabase() { return this.healthService.checkDatabaseMinimal(); } + + @Get('sentry-test') + @ApiOperation({ summary: 'Trigger a deliberate error to verify Sentry integration' }) + @ApiResponse({ status: 500, description: 'Sentry test error triggered successfully' }) + async triggerSentryTest() { + throw new Error('sentry test'); + } } diff --git a/src/modules/metrics/metrics.module.ts b/src/modules/metrics/metrics.module.ts index 9b2d8be..92c7e49 100644 --- a/src/modules/metrics/metrics.module.ts +++ b/src/modules/metrics/metrics.module.ts @@ -1,4 +1,4 @@ -import { Module } from '@nestjs/common'; +import { Module, Global } from '@nestjs/common'; import { APP_INTERCEPTOR } from '@nestjs/core'; import { BullModule } from '@nestjs/bullmq'; import { PrometheusModule } from '@willsoto/nestjs-prometheus'; @@ -8,6 +8,7 @@ import { MetricsInterceptor } from './metrics.interceptor'; import { MetricsUpdater } from './metrics.updater'; import { SupabaseService } from '../../database/supabase.client'; +@Global() @Module({ imports: [ PrometheusModule.register({ @@ -23,7 +24,7 @@ import { SupabaseService } from '../../database/supabase.client'; { name: 'nonce-cleanup' }, ), ], - controllers: [MetricsController], + controllers: [], providers: [ ...metricProviders, MetricsService, diff --git a/src/modules/metrics/metrics.service.ts b/src/modules/metrics/metrics.service.ts index 43bfba1..e3f9830 100644 --- a/src/modules/metrics/metrics.service.ts +++ b/src/modules/metrics/metrics.service.ts @@ -1,6 +1,7 @@ -import { Inject, Injectable } from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; import { Counter, Gauge, Histogram } from 'prom-client'; import { + InjectMetric, makeCounterProvider, makeGaugeProvider, makeHistogramProvider, @@ -47,17 +48,17 @@ export const metricProviders = [ @Injectable() export class MetricsService { constructor( - @Inject(HTTP_REQUEST_COUNT) + @InjectMetric(HTTP_REQUEST_COUNT) private readonly requestCounter: Counter, - @Inject(HTTP_REQUEST_DURATION_SECONDS) + @InjectMetric(HTTP_REQUEST_DURATION_SECONDS) private readonly requestDuration: Histogram, - @Inject(BULLMQ_QUEUE_DEPTH) + @InjectMetric(BULLMQ_QUEUE_DEPTH) private readonly queueDepth: Gauge, - @Inject(INDEXER_LAG) + @InjectMetric(INDEXER_LAG) private readonly indexerLag: Gauge, - @Inject(HORIZON_HEALTH) + @InjectMetric(HORIZON_HEALTH) private readonly horizonHealth: Gauge, - @Inject(DB_POOL_OPEN) + @InjectMetric(DB_POOL_OPEN) private readonly dbPoolOpen: Gauge, ) {}