Skip to content

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 api succeeds
  • pnpm nx build web succeeds
  • pnpm lint passes 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

  1. Create observability library in libs/observability
  2. Install dependencies (@sentry/nestjs, @sentry/react, nestjs-pino, etc.)
  3. Create instrument.ts for backend Sentry initialization
  4. Update main.ts to import instrument first
  5. Create ObservabilityModule with Pino logger
  6. Create exception filter with Sentry capture
  7. Create logging interceptor with correlation IDs
  8. Update Prisma service with Sentry breadcrumbs
  9. Create frontend sentry.ts initialization
  10. Create ErrorBoundary component
  11. Update main.tsx to use Sentry
  12. Write unit tests for all components
  13. Update environment configuration
  14. Update README.md with observability documentation
  15. 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

  1. Navigate to Sentry project dashboard
  2. Verify error events are captured with:
  3. Stack traces
  4. Request context
  5. Breadcrumbs
  6. Tags (environment, app, component)
  7. Verify performance traces show:
  8. HTTP request spans
  9. Database query spans
  10. 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.