AI Prompt: Forma3D.Connect — Phase 1b: Sentry Observability ✅¶
Purpose: This prompt instructs an AI to implement Phase 1b of Forma3D.Connect
Estimated Effort: 16 hours
Prerequisites: Phase 1 completed (Shopify integration, order storage, product mappings)
Output: Production-grade observability with Sentry, OpenTelemetry, and structured logging
Status: ✅ COMPLETED — January 10, 2026
🎯 Mission¶
You are continuing development of Forma3D.Connect, building on the Phase 1 foundation. Your task is to implement Phase 1b: Sentry Observability — establishing comprehensive observability with error tracking, performance monitoring, and structured logging.
Phase 1b delivers:
- Error and exception tracking (backend + frontend)
- Performance monitoring with distributed tracing
- Structured JSON logging with correlation IDs
- OpenTelemetry-first architecture for vendor neutrality
📋 Phase 1b Context¶
What Was Built in Phase 0 & 1¶
The foundation is already in place:
- Nx monorepo with
apps/api,apps/web, and shared libs - PostgreSQL database with Prisma schema
- NestJS backend with Shopify webhooks, order storage, product mappings
- React 19 frontend with basic dashboard
- Azure DevOps CI/CD pipeline
- OpenAPI/Swagger documentation at
/api/docs - Aikido Security Platform for vulnerability scanning
What Phase 1b Builds¶
| Feature | Description | Effort |
|---|---|---|
| F1b.1: Backend Observability | Sentry + OpenTelemetry for NestJS | 8 hours |
| F1b.2: Frontend Observability | Sentry for React with error boundaries | 4 hours |
| F1b.3: Structured Logging | JSON logs with correlation and trace IDs | 2 hours |
| F1b.4: Observability Library | Shared observability configuration in libs/ | 2 hours |
🛠️ Tech Stack Reference¶
All technologies from Phase 1 remain. Additional packages for Phase 1b:
| Package | Purpose |
|---|---|
@sentry/nestjs |
Sentry SDK for NestJS |
@sentry/node |
Sentry Node.js core SDK |
@sentry/profiling-node |
Node.js profiling for Sentry |
@sentry/react |
Sentry SDK for React |
@opentelemetry/sdk-node |
OpenTelemetry Node.js SDK |
@opentelemetry/auto-instrumentations-node |
Auto-instrumentation for common libs |
@opentelemetry/exporter-trace-otlp-http |
OTLP trace exporter |
@prisma/instrumentation |
OpenTelemetry instrumentation for Prisma |
pino |
High-performance JSON logger |
pino-pretty |
Pretty printing for development |
nestjs-pino |
Pino integration for NestJS |
Mind a Sentry account for this project has already been created. The onboarding process and necessary keys and tokens can be found in "docs/prompts/prompt-phase1b-observability-onboarding.png"
📁 New Files to Create¶
Add to the existing structure:
libs/observability/
├── src/
│ ├── index.ts # Public exports
│ ├── lib/
│ │ ├── sentry.config.ts # Shared Sentry configuration
│ │ ├── otel.config.ts # OpenTelemetry configuration
│ │ └── constants.ts # Observability constants
│ └── types/
│ └── observability.types.ts # Type definitions
├── package.json
├── project.json
├── tsconfig.json
├── tsconfig.lib.json
├── eslint.config.mjs
└── README.md
apps/api/src/
├── observability/
│ ├── observability.module.ts # Observability module
│ ├── instrument.ts # Sentry + OTEL initialization (runs first)
│ └── filters/
│ └── sentry-exception.filter.ts # Global exception filter with Sentry
│ └── interceptors/
│ └── logging.interceptor.ts # Request/response logging
apps/web/src/
├── observability/
│ ├── sentry.ts # Sentry initialization
│ └── ErrorBoundary.tsx # Sentry error boundary component
🔧 Feature F1b.1: Backend Observability¶
Requirements Reference¶
- NFR-MA-004: Comprehensive Logging
- NFR-OB-001: Error Tracking (new)
- NFR-OB-002: Performance Monitoring (new)
- NFR-OB-003: Distributed Tracing (new)
Implementation¶
1. Create Observability Library¶
Create libs/observability/src/lib/sentry.config.ts:
/**
* Shared Sentry configuration for Forma3D.Connect
* Used by both backend and frontend applications
*/
export interface SentryConfig {
dsn: string;
environment: string;
release?: string;
debug?: boolean;
tracesSampleRate: number;
profilesSampleRate: number;
}
export function getSentryConfig(overrides?: Partial<SentryConfig>): SentryConfig {
const environment = process.env['NODE_ENV'] || 'development';
const isProduction = environment === 'production';
return {
dsn: process.env['SENTRY_DSN'] || '',
environment,
release:
process.env['SENTRY_RELEASE'] ||
`forma3d-connect@${process.env['npm_package_version'] || '0.0.0'}`,
debug: !isProduction,
// Free tier compatible: 10% traces in production, 100% in development
tracesSampleRate: isProduction ? 0.1 : 1.0,
profilesSampleRate: isProduction ? 0.1 : 1.0,
...overrides,
};
}
export const SENTRY_IGNORED_ERRORS = [
// Common non-actionable errors
'ResizeObserver loop limit exceeded',
'Network request failed',
'Load failed',
];
export const SENTRY_SENSITIVE_FIELDS = [
'password',
'token',
'secret',
'apiKey',
'authorization',
'cookie',
];
Create libs/observability/src/lib/otel.config.ts:
/**
* OpenTelemetry configuration for vendor-neutral instrumentation
*/
export interface OtelConfig {
serviceName: string;
serviceVersion: string;
environment: string;
exporterEndpoint?: string;
enableConsoleExporter: boolean;
}
export function getOtelConfig(serviceName: string, overrides?: Partial<OtelConfig>): OtelConfig {
const environment = process.env['NODE_ENV'] || 'development';
return {
serviceName,
serviceVersion: process.env['npm_package_version'] || '0.0.0',
environment,
exporterEndpoint: process.env['OTEL_EXPORTER_OTLP_ENDPOINT'],
enableConsoleExporter: environment === 'development',
...overrides,
};
}
Create libs/observability/src/lib/constants.ts:
export const TRACE_ID_HEADER = 'x-trace-id';
export const REQUEST_ID_HEADER = 'x-request-id';
export const SPAN_ID_HEADER = 'x-span-id';
export const OBSERVABILITY_MODULE_OPTIONS = 'OBSERVABILITY_MODULE_OPTIONS';
Create libs/observability/src/index.ts:
export * from './lib/sentry.config';
export * from './lib/otel.config';
export * from './lib/constants';
export * from './types/observability.types';
2. Sentry + OpenTelemetry Instrumentation (Backend)¶
Create apps/api/src/observability/instrument.ts:
/**
* Sentry and OpenTelemetry instrumentation
* MUST be imported before any other imports in main.ts
*/
import * as Sentry from '@sentry/nestjs';
import { nodeProfilingIntegration } from '@sentry/profiling-node';
import { getSentryConfig, SENTRY_IGNORED_ERRORS } from '@forma3d/observability';
const config = getSentryConfig();
// Only initialize if DSN is provided
if (config.dsn) {
Sentry.init({
dsn: config.dsn,
environment: config.environment,
release: config.release,
debug: config.debug,
// Performance Monitoring
tracesSampleRate: config.tracesSampleRate,
// Profiling
profilesSampleRate: config.profilesSampleRate,
integrations: [nodeProfilingIntegration()],
// Filter out noisy errors
ignoreErrors: SENTRY_IGNORED_ERRORS,
// Scrub sensitive data
beforeSend(event) {
// Remove sensitive headers
if (event.request?.headers) {
delete event.request.headers['authorization'];
delete event.request.headers['cookie'];
delete event.request.headers['x-shopify-access-token'];
}
return event;
},
// Add custom tags
initialScope: {
tags: {
app: 'api',
component: 'backend',
},
},
});
console.log(`[Sentry] Initialized for environment: ${config.environment}`);
}
export { Sentry };
3. Update main.ts¶
Update apps/api/src/main.ts to import instrumentation first:
// MUST be first import
import './observability/instrument';
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app/app.module';
import { Logger, ValidationPipe } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import * as Sentry from '@sentry/nestjs';
import { Logger as PinoLogger } from 'nestjs-pino';
async function bootstrap() {
const app = await NestFactory.create(AppModule, {
rawBody: true,
bufferLogs: true, // Buffer logs until Pino is ready
});
// Use Pino for logging
app.useLogger(app.get(PinoLogger));
const configService = app.get(ConfigService);
const port = configService.get<number>('APP_PORT', 3000);
const frontendUrl = configService.get<string>('FRONTEND_URL', 'http://localhost:4200');
// Enable CORS
app.enableCors({
origin: frontendUrl,
credentials: true,
});
// Global validation pipe
app.useGlobalPipes(
new ValidationPipe({
whitelist: true,
forbidNonWhitelisted: true,
transform: true,
})
);
// Sentry error handler - must be after all controllers
app.useGlobalFilters(new Sentry.SentryGlobalFilter());
await app.listen(port);
Logger.log(`🚀 Application is running on: http://localhost:${port}`);
}
bootstrap();
4. Observability Module¶
Create apps/api/src/observability/observability.module.ts:
import { Module, Global } from '@nestjs/common';
import { APP_INTERCEPTOR, APP_FILTER } from '@nestjs/core';
import { SentryModule } from '@sentry/nestjs/setup';
import { LoggerModule } from 'nestjs-pino';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { LoggingInterceptor } from './interceptors/logging.interceptor';
import { SentryExceptionFilter } from './filters/sentry-exception.filter';
import { TRACE_ID_HEADER, REQUEST_ID_HEADER } from '@forma3d/observability';
import { randomUUID } from 'crypto';
@Global()
@Module({
imports: [
SentryModule.forRoot(),
LoggerModule.forRootAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: (configService: ConfigService) => {
const isProduction = configService.get('NODE_ENV') === 'production';
return {
pinoHttp: {
level: isProduction ? 'info' : 'debug',
transport: isProduction
? undefined
: {
target: 'pino-pretty',
options: {
colorize: true,
singleLine: true,
},
},
genReqId: (req) => {
// Use existing trace ID or generate new one
return req.headers[TRACE_ID_HEADER] || req.headers[REQUEST_ID_HEADER] || randomUUID();
},
customProps: (req) => ({
traceId: req.id,
environment: configService.get('NODE_ENV'),
}),
redact: {
paths: [
'req.headers.authorization',
'req.headers.cookie',
'req.headers["x-shopify-access-token"]',
'req.body.password',
'req.body.token',
],
remove: true,
},
serializers: {
req: (req) => ({
id: req.id,
method: req.method,
url: req.url,
query: req.query,
params: req.params,
}),
res: (res) => ({
statusCode: res.statusCode,
}),
},
},
};
},
}),
],
providers: [
{
provide: APP_INTERCEPTOR,
useClass: LoggingInterceptor,
},
{
provide: APP_FILTER,
useClass: SentryExceptionFilter,
},
],
exports: [],
})
export class ObservabilityModule {}
5. Logging Interceptor¶
Create apps/api/src/observability/interceptors/logging.interceptor.ts:
import { Injectable, NestInterceptor, ExecutionContext, CallHandler, Logger } from '@nestjs/common';
import { Observable } from 'rxjs';
import { tap, catchError } from 'rxjs/operators';
import * as Sentry from '@sentry/nestjs';
@Injectable()
export class LoggingInterceptor implements NestInterceptor {
private readonly logger = new Logger(LoggingInterceptor.name);
intercept(context: ExecutionContext, next: CallHandler): Observable<unknown> {
const request = context.switchToHttp().getRequest();
const { method, url, body, headers } = request;
const correlationId = request.id || headers['x-request-id'];
const startTime = Date.now();
// Add breadcrumb for request
Sentry.addBreadcrumb({
category: 'http',
message: `${method} ${url}`,
level: 'info',
data: {
correlationId,
method,
url,
},
});
return next.handle().pipe(
tap((response) => {
const duration = Date.now() - startTime;
this.logger.log({
message: `${method} ${url} completed`,
correlationId,
duration,
statusCode: context.switchToHttp().getResponse().statusCode,
});
}),
catchError((error) => {
const duration = Date.now() - startTime;
this.logger.error({
message: `${method} ${url} failed`,
correlationId,
duration,
error: error.message,
stack: error.stack,
});
throw error;
})
);
}
}
6. Sentry Exception Filter¶
Create apps/api/src/observability/filters/sentry-exception.filter.ts:
import {
ExceptionFilter,
Catch,
ArgumentsHost,
HttpException,
HttpStatus,
Logger,
} from '@nestjs/common';
import * as Sentry from '@sentry/nestjs';
import { Request, Response } from 'express';
@Catch()
export class SentryExceptionFilter implements ExceptionFilter {
private readonly logger = new Logger(SentryExceptionFilter.name);
catch(exception: unknown, host: ArgumentsHost): void {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
const request = ctx.getRequest<Request>();
const status =
exception instanceof HttpException ? exception.getStatus() : HttpStatus.INTERNAL_SERVER_ERROR;
const message =
exception instanceof HttpException ? exception.message : 'Internal server error';
// Only capture 5xx errors to Sentry (avoid noisy 4xx)
if (status >= 500) {
Sentry.withScope((scope) => {
scope.setTag('status_code', status.toString());
scope.setContext('request', {
method: request.method,
url: request.url,
headers: request.headers,
query: request.query,
body: this.sanitizeBody(request.body),
});
if (exception instanceof Error) {
Sentry.captureException(exception);
} else {
Sentry.captureMessage(String(exception), 'error');
}
});
}
// Log the error
this.logger.error({
message: `Exception: ${message}`,
status,
path: request.url,
method: request.method,
correlationId: (request as Request & { id?: string }).id,
stack: exception instanceof Error ? exception.stack : undefined,
});
response.status(status).json({
statusCode: status,
message,
timestamp: new Date().toISOString(),
path: request.url,
});
}
private sanitizeBody(body: Record<string, unknown>): Record<string, unknown> {
if (!body || typeof body !== 'object') return body;
const sanitized = { ...body };
const sensitiveFields = ['password', 'token', 'secret', 'apiKey'];
for (const field of sensitiveFields) {
if (field in sanitized) {
sanitized[field] = '[REDACTED]';
}
}
return sanitized;
}
}
7. Prisma Instrumentation¶
Update Prisma service for tracing. Modify apps/api/src/database/prisma.service.ts:
import { Injectable, OnModuleInit, OnModuleDestroy, Logger } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';
import * as Sentry from '@sentry/nestjs';
@Injectable()
export class PrismaService extends PrismaClient implements OnModuleInit, OnModuleDestroy {
private readonly logger = new Logger(PrismaService.name);
constructor() {
super({
log: [
{ emit: 'event', level: 'query' },
{ emit: 'event', level: 'error' },
{ emit: 'event', level: 'warn' },
],
});
// Add Sentry breadcrumbs for database queries
this.$on('query', (e) => {
Sentry.addBreadcrumb({
category: 'database',
message: 'Prisma Query',
level: 'info',
data: {
query: e.query,
duration: e.duration,
},
});
});
this.$on('error', (e) => {
this.logger.error(`Prisma Error: ${e.message}`);
Sentry.captureException(new Error(`Prisma Error: ${e.message}`));
});
}
async onModuleInit() {
await this.$connect();
this.logger.log('Database connection established');
}
async onModuleDestroy() {
await this.$disconnect();
this.logger.log('Database connection closed');
}
}
🔧 Feature F1b.2: Frontend Observability¶
Implementation¶
1. Sentry Initialization¶
Create apps/web/src/observability/sentry.ts:
import * as Sentry from '@sentry/react';
import { getSentryConfig, SENTRY_IGNORED_ERRORS } from '@forma3d/observability';
const config = getSentryConfig();
export function initSentry(): void {
if (!config.dsn) {
console.warn('[Sentry] No DSN provided, skipping initialization');
return;
}
Sentry.init({
dsn: config.dsn,
environment: config.environment,
release: config.release,
debug: config.debug,
// Performance Monitoring
tracesSampleRate: config.tracesSampleRate,
// Session Replay (optional, disabled for free tier)
replaysSessionSampleRate: 0,
replaysOnErrorSampleRate: 0,
// Integrations
integrations: [
Sentry.browserTracingIntegration(),
Sentry.replayIntegration({
maskAllText: true,
blockAllMedia: true,
}),
],
// Filter errors
ignoreErrors: SENTRY_IGNORED_ERRORS,
// Scrub sensitive data
beforeSend(event) {
// Remove PII from breadcrumbs
if (event.breadcrumbs) {
event.breadcrumbs = event.breadcrumbs.map((breadcrumb) => {
if (breadcrumb.data?.url) {
// Remove query params that might contain tokens
const url = new URL(breadcrumb.data.url, window.location.origin);
url.searchParams.delete('token');
url.searchParams.delete('key');
breadcrumb.data.url = url.toString();
}
return breadcrumb;
});
}
return event;
},
// Tag all events
initialScope: {
tags: {
app: 'web',
component: 'frontend',
},
},
});
console.log(`[Sentry] Initialized for environment: ${config.environment}`);
}
// Export Sentry for manual error capture
export { Sentry };
2. Error Boundary Component¶
Create apps/web/src/observability/ErrorBoundary.tsx:
import * as Sentry from '@sentry/react';
import { Component, ReactNode } from 'react';
interface Props {
children: ReactNode;
fallback?: ReactNode;
}
interface State {
hasError: boolean;
eventId?: string;
}
export class ErrorBoundary extends Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(): State {
return { hasError: true };
}
componentDidCatch(error: Error, errorInfo: React.ErrorInfo): void {
const eventId = Sentry.captureException(error, {
extra: {
componentStack: errorInfo.componentStack,
},
});
this.setState({ eventId });
}
handleReportClick = (): void => {
if (this.state.eventId) {
Sentry.showReportDialog({ eventId: this.state.eventId });
}
};
handleRetryClick = (): void => {
this.setState({ hasError: false, eventId: undefined });
};
render(): ReactNode {
if (this.state.hasError) {
if (this.props.fallback) {
return this.props.fallback;
}
return (
<div className="min-h-screen flex items-center justify-center bg-gray-50">
<div className="max-w-md w-full p-8 bg-white rounded-lg shadow-lg text-center">
<h2 className="text-2xl font-bold text-gray-900 mb-4">Something went wrong</h2>
<p className="text-gray-600 mb-6">
We've been notified and are working to fix the issue.
</p>
<div className="space-x-4">
<button
onClick={this.handleRetryClick}
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700"
>
Try Again
</button>
<button
onClick={this.handleReportClick}
className="px-4 py-2 border border-gray-300 rounded-lg hover:bg-gray-50"
>
Report Issue
</button>
</div>
</div>
</div>
);
}
return this.props.children;
}
}
// Functional wrapper for Sentry's error boundary
export const SentryErrorBoundary = Sentry.withErrorBoundary(
({ children }: { children: ReactNode }) => <>{children}</>,
{
showDialog: true,
}
);
3. Update main.tsx¶
Update apps/web/src/main.tsx:
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import { initSentry } from './observability/sentry';
import { ErrorBoundary } from './observability/ErrorBoundary';
import App from './App';
import './index.css';
// Initialize Sentry before rendering
initSentry();
createRoot(document.getElementById('root')!).render(
<StrictMode>
<ErrorBoundary>
<App />
</ErrorBoundary>
</StrictMode>
);
🔧 Feature F1b.3: Custom Error Capture Examples¶
Backend Usage Examples¶
import * as Sentry from '@sentry/nestjs';
// Capture custom error with context
Sentry.captureException(new Error('Payment processing failed'), {
tags: {
orderId: '12345',
paymentProvider: 'stripe',
},
extra: {
amount: 99.99,
currency: 'EUR',
},
user: {
email: 'customer@example.com',
},
});
// Add breadcrumbs for debugging
Sentry.addBreadcrumb({
category: 'order',
message: 'Order status changed',
level: 'info',
data: {
orderId: '12345',
previousStatus: 'pending',
newStatus: 'processing',
},
});
// Set user context
Sentry.setUser({
id: 'operator-1',
email: 'operator@forma3d.be',
role: 'admin',
});
// Set custom tags
Sentry.setTags({
feature: 'shopify-integration',
version: '1.0.0',
});
Frontend Usage Examples¶
import { Sentry } from './observability/sentry';
// Capture error in try-catch
try {
await fetchOrders();
} catch (error) {
Sentry.captureException(error, {
tags: { component: 'OrderList' },
});
// Handle error in UI
}
// Capture custom message
Sentry.captureMessage('User attempted unauthorized action', 'warning');
// Add navigation breadcrumb
Sentry.addBreadcrumb({
category: 'navigation',
message: `Navigated to ${location.pathname}`,
level: 'info',
});
📦 Module Configuration¶
Update App Module¶
Update apps/api/src/app/app.module.ts:
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { EventEmitterModule } from '@nestjs/event-emitter';
import { ObservabilityModule } from '../observability/observability.module';
// ... other imports
@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true,
envFilePath: ['.env.local', '.env'],
}),
EventEmitterModule.forRoot(),
ObservabilityModule, // Add early in imports
// ... other modules
],
// ...
})
export class AppModule {}
🔧 Environment Variables¶
Add to .env.example:
# Sentry Configuration
SENTRY_DSN=https://your-dsn@sentry.io/project-id
SENTRY_RELEASE=forma3d-connect@1.0.0
SENTRY_ENVIRONMENT=development
# OpenTelemetry (optional, for custom exporters)
OTEL_EXPORTER_OTLP_ENDPOINT=
OTEL_SERVICE_NAME=forma3d-api
🧪 Testing Requirements¶
Test Coverage Requirements¶
Per requirements.md (NFR-MA-002):
- Unit Tests: > 80% coverage
- Integration Tests: All observability integrations tested
Test File Structure¶
apps/api/src/observability/
├── __tests__/
│ ├── observability.module.spec.ts
│ ├── sentry-exception.filter.spec.ts
│ └── logging.interceptor.spec.ts
libs/observability/src/
├── __tests__/
│ ├── sentry.config.spec.ts
│ └── otel.config.spec.ts
Unit Test Example¶
Create libs/observability/src/__tests__/sentry.config.spec.ts:
import { getSentryConfig, SENTRY_IGNORED_ERRORS } from '../lib/sentry.config';
describe('getSentryConfig', () => {
const originalEnv = process.env;
beforeEach(() => {
process.env = { ...originalEnv };
});
afterAll(() => {
process.env = originalEnv;
});
it('should return default config when no env vars set', () => {
const config = getSentryConfig();
expect(config.dsn).toBe('');
expect(config.environment).toBe('test');
expect(config.tracesSampleRate).toBe(1.0); // Non-production
});
it('should use production sample rates in production', () => {
process.env['NODE_ENV'] = 'production';
const config = getSentryConfig();
expect(config.tracesSampleRate).toBe(0.1);
expect(config.profilesSampleRate).toBe(0.1);
expect(config.debug).toBe(false);
});
it('should apply overrides', () => {
const config = getSentryConfig({
tracesSampleRate: 0.5,
dsn: 'custom-dsn',
});
expect(config.tracesSampleRate).toBe(0.5);
expect(config.dsn).toBe('custom-dsn');
});
it('should have correct ignored errors', () => {
expect(SENTRY_IGNORED_ERRORS).toContain('ResizeObserver loop limit exceeded');
expect(SENTRY_IGNORED_ERRORS).toContain('Network request failed');
});
});
✅ Validation Checklist¶
Infrastructure¶
- Observability library created in
libs/observability - All new modules compile without errors
-
pnpm nx build apisucceeds -
pnpm nx build websucceeds -
pnpm lintpasses on all new files
Backend Observability (F1b.1)¶
- Sentry initialized before app bootstrap
- Errors captured and sent to Sentry
- Performance traces recorded
- Prisma queries traced
- Structured JSON logging working
- Correlation IDs propagated
Frontend Observability (F1b.2)¶
- Sentry initialized on app load
- Error boundaries catch React errors
- Page navigation traced
- API calls correlated with backend traces
Testing¶
- Unit tests pass:
pnpm nx test observability - Unit tests pass:
pnpm nx test api - Unit tests pass:
pnpm nx test web
🚫 Constraints and Rules¶
MUST DO¶
- Initialize Sentry BEFORE any other imports in main.ts
- Scrub sensitive data (passwords, tokens, PII) from logs and Sentry
- Use correlation IDs for request tracing
- Configure sampling rates compatible with Sentry Free Tier
- Log all errors to both Sentry and application logs
MUST NOT¶
- Store Sentry DSN in source code
- Capture 4xx errors to Sentry (too noisy)
- Log sensitive data (passwords, tokens, API keys)
- Exceed Sentry Free Tier limits (10,000 errors/month)
- Block application startup if Sentry is unavailable
🎬 Execution Order¶
- Create observability library in
libs/observability - Install dependencies (
@sentry/nestjs,@sentry/react,nestjs-pino, etc.) - Create instrument.ts for backend Sentry initialization
- Update main.ts to import instrument first
- Create ObservabilityModule with Pino logger
- Create exception filter with Sentry capture
- Create logging interceptor with correlation IDs
- Update Prisma service with Sentry breadcrumbs
- Create frontend sentry.ts initialization
- Create ErrorBoundary component
- Update main.tsx to use Sentry
- Write unit tests for all components
- Update environment configuration
- Update README.md with observability documentation
- Run full validation checklist
📊 Expected Output¶
When Phase 1b is complete:
Verification Commands¶
# Build all projects
pnpm nx build api && pnpm nx build web
# Run tests
pnpm nx test observability
pnpm nx test api
pnpm nx test web
# Start API and verify logs
pnpm nx serve api
# Should see structured JSON logs with trace IDs
# Trigger an error and verify Sentry
curl -X POST http://localhost:3000/api/test-error
# Check Sentry dashboard for captured error
Sentry Dashboard Verification¶
- Navigate to Sentry project dashboard
- Verify error events are captured with:
- Stack traces
- Request context
- Breadcrumbs
- Tags (environment, app, component)
- Verify performance traces show:
- HTTP request spans
- Database query spans
- Custom spans
🔗 Phase 1b Exit Criteria¶
- Sentry SDK integrated in backend and frontend
- Errors captured with context and stack traces
- Performance monitoring enabled
- Structured JSON logging with correlation IDs
- Prisma queries traced
- Free Tier compatible sampling rates
- Sensitive data scrubbed from logs and Sentry
- Unit tests passing
- Documentation updated in README.md
END OF PROMPT
This prompt builds on the Phase 1 foundation. The AI should implement all Phase 1b observability features while maintaining the established code style, architectural patterns, and testing standards.