# NestJS Integration Guide Server-side analytics tracking for NestJS applications. ## Installation ```bash npm install @analytics/client ``` ## Module Setup ### 1. Create Analytics Module ```typescript // analytics.module.ts import { Module, Global, DynamicModule } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { BackendAnalyticsClient, type BackendAnalyticsConfig } from '@analytics/client'; export const ANALYTICS_CLIENT = 'ANALYTICS_CLIENT'; export interface AnalyticsModuleOptions { collectorUrl: string; appName: string; enabled?: boolean; debug?: boolean; } @Global() @Module({}) export class AnalyticsModule { static register(options: AnalyticsModuleOptions): DynamicModule { return { module: AnalyticsModule, providers: [ { provide: ANALYTICS_CLIENT, useFactory: () => { const config: BackendAnalyticsConfig = { apiBaseUrl: options.collectorUrl, appName: options.appName, enabled: options.enabled ?? true, enableDebugLogging: options.debug ?? false, }; return new BackendAnalyticsClient(config); }, }, ], exports: [ANALYTICS_CLIENT], }; } static registerAsync(options: { inject?: any[]; useFactory: (...args: any[]) => AnalyticsModuleOptions | Promise; }): DynamicModule { return { module: AnalyticsModule, providers: [ { provide: ANALYTICS_CLIENT, inject: options.inject || [], useFactory: async (...args: any[]) => { const moduleOptions = await options.useFactory(...args); const config: BackendAnalyticsConfig = { apiBaseUrl: moduleOptions.collectorUrl, appName: moduleOptions.appName, enabled: moduleOptions.enabled ?? true, enableDebugLogging: moduleOptions.debug ?? false, }; return new BackendAnalyticsClient(config); }, }, ], exports: [ANALYTICS_CLIENT], }; } } ``` ### 2. Register in AppModule ```typescript // app.module.ts import { Module } from '@nestjs/common'; import { ConfigModule, ConfigService } from '@nestjs/config'; import { AnalyticsModule } from './analytics.module'; @Module({ imports: [ ConfigModule.forRoot({ isGlobal: true }), AnalyticsModule.registerAsync({ inject: [ConfigService], useFactory: (config: ConfigService) => ({ collectorUrl: config.get('ANALYTICS_URL', 'http://localhost:4001'), appName: config.get('APP_NAME', 'my-api'), enabled: config.get('NODE_ENV') !== 'test', debug: config.get('NODE_ENV') === 'development', }), }), ], }) export class AppModule {} ``` ## Request Tracking Interceptor Automatically track all HTTP requests. ```typescript // analytics.interceptor.ts import { Injectable, NestInterceptor, ExecutionContext, CallHandler, Inject, } from '@nestjs/common'; import { Observable } from 'rxjs'; import { tap, catchError } from 'rxjs/operators'; import { BackendAnalyticsClient } from '@analytics/client'; import { ANALYTICS_CLIENT } from './analytics.module'; @Injectable() export class AnalyticsInterceptor implements NestInterceptor { constructor( @Inject(ANALYTICS_CLIENT) private readonly analytics: BackendAnalyticsClient, ) {} intercept(context: ExecutionContext, next: CallHandler): Observable { const request = context.switchToHttp().getRequest(); const response = context.switchToHttp().getResponse(); const startTime = Date.now(); const sessionId = request.headers['x-session-id'] || this.generateSessionId(); const userId = request.user?.id; return next.handle().pipe( tap(() => { this.trackRequest(request, response, sessionId, userId, startTime, 'success'); }), catchError((error) => { this.trackRequest(request, response, sessionId, userId, startTime, 'error', error); throw error; }), ); } private generateSessionId(): string { return `srv_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`; } private trackRequest( request: any, response: any, sessionId: string, userId: string | undefined, startTime: number, outcome: 'success' | 'error', error?: Error, ): void { const duration = Date.now() - startTime; this.analytics.trackEngagement({ sessionId, userId, type: 'api_request', action: `${request.method} ${request.route?.path || request.url}`, metadata: { method: request.method, path: request.route?.path || request.url, statusCode: response.statusCode, durationMs: duration, outcome, errorMessage: error?.message, ip: this.extractIp(request), userAgent: request.headers['user-agent'], }, }); } private extractIp(request: any): string { return ( request.headers['x-forwarded-for']?.split(',')[0]?.trim() || request.headers['x-real-ip'] || request.ip || '0.0.0.0' ); } } ``` ### Register Globally ```typescript // app.module.ts import { APP_INTERCEPTOR } from '@nestjs/core'; import { AnalyticsInterceptor } from './analytics.interceptor'; @Module({ providers: [ { provide: APP_INTERCEPTOR, useClass: AnalyticsInterceptor, }, ], }) export class AppModule {} ``` ## Method Decorator Track specific endpoints with custom events. ```typescript // track-analytics.decorator.ts import { SetMetadata } from '@nestjs/common'; export const TRACK_ANALYTICS_KEY = 'track_analytics'; export interface TrackAnalyticsOptions { event: string; category?: string; metadata?: Record; extractMetadata?: (context: { request: any; response: any; result: any; }) => Record; } export const TrackAnalytics = (options: TrackAnalyticsOptions) => SetMetadata(TRACK_ANALYTICS_KEY, options); ``` ### Usage ```typescript // user.controller.ts import { Controller, Post, Body } from '@nestjs/common'; import { TrackAnalytics } from './track-analytics.decorator'; @Controller('users') export class UserController { @Post('signup') @TrackAnalytics({ event: 'user_signup', category: 'auth', extractMetadata: ({ result }) => ({ userId: result.id, plan: result.plan, }), }) async signup(@Body() dto: SignupDto) { // Your signup logic return { id: 'user-123', plan: 'free' }; } } ``` ## Service-Level Tracking Track events directly from services. ```typescript // order.service.ts import { Injectable, Inject } from '@nestjs/common'; import { BackendAnalyticsClient } from '@analytics/client'; import { ANALYTICS_CLIENT } from './analytics.module'; @Injectable() export class OrderService { constructor( @Inject(ANALYTICS_CLIENT) private readonly analytics: BackendAnalyticsClient, ) {} async createOrder(dto: CreateOrderDto): Promise { const order = await this.orderRepo.save(dto); // Track order creation this.analytics.trackEngagement({ sessionId: dto.sessionId || 'server', userId: dto.userId, type: 'commerce', action: 'order_created', metadata: { orderId: order.id, total: order.total, itemCount: order.items.length, }, }); return order; } async processPayment(orderId: string, paymentMethod: string): Promise { const order = await this.orderRepo.findOne(orderId); // Process payment... // Track conversion this.analytics.trackEngagement({ sessionId: 'server', userId: order.userId, type: 'commerce', action: 'payment_completed', metadata: { orderId, total: order.total, paymentMethod, isConversion: true, conversionValue: order.total, }, }); return order; } } ``` ## WebSocket Tracking Track WebSocket events. ```typescript // events.gateway.ts import { WebSocketGateway, SubscribeMessage, ConnectedSocket, OnGatewayConnection, OnGatewayDisconnect, } from '@nestjs/websockets'; import { Inject } from '@nestjs/common'; import { Socket } from 'socket.io'; import { BackendAnalyticsClient } from '@analytics/client'; import { ANALYTICS_CLIENT } from './analytics.module'; @WebSocketGateway() export class EventsGateway implements OnGatewayConnection, OnGatewayDisconnect { constructor( @Inject(ANALYTICS_CLIENT) private readonly analytics: BackendAnalyticsClient, ) {} handleConnection(client: Socket) { const sessionId = client.handshake.query.sessionId as string; this.analytics.trackEngagement({ sessionId: sessionId || client.id, type: 'websocket', action: 'connected', metadata: { socketId: client.id, transport: client.conn.transport.name, }, }); } handleDisconnect(client: Socket) { this.analytics.trackEngagement({ sessionId: client.id, type: 'websocket', action: 'disconnected', metadata: { socketId: client.id, reason: client.disconnected ? 'client' : 'server', }, }); } @SubscribeMessage('message') handleMessage(@ConnectedSocket() client: Socket, payload: any) { this.analytics.trackEngagement({ sessionId: client.id, type: 'websocket', action: 'message_received', metadata: { messageType: payload.type, }, }); } } ``` ## Queue Job Tracking Track background job execution. ```typescript // email.processor.ts import { Processor, Process } from '@nestjs/bull'; import { Inject } from '@nestjs/common'; import { Job } from 'bull'; import { BackendAnalyticsClient } from '@analytics/client'; import { ANALYTICS_CLIENT } from './analytics.module'; @Processor('email') export class EmailProcessor { constructor( @Inject(ANALYTICS_CLIENT) private readonly analytics: BackendAnalyticsClient, ) {} @Process('send') async handleSend(job: Job<{ to: string; template: string }>) { const startTime = Date.now(); try { await this.sendEmail(job.data); this.analytics.trackEngagement({ sessionId: 'queue', type: 'background_job', action: 'email_sent', metadata: { jobId: job.id, template: job.data.template, durationMs: Date.now() - startTime, success: true, }, }); } catch (error) { this.analytics.trackEngagement({ sessionId: 'queue', type: 'background_job', action: 'email_failed', metadata: { jobId: job.id, template: job.data.template, durationMs: Date.now() - startTime, success: false, error: error.message, }, }); throw error; } } } ``` ## Best Practices ### 1. Use Session ID Header Pass session ID from frontend: ```typescript // Frontend fetch('/api/orders', { headers: { 'x-session-id': analytics.getSessionId(), }, }); ``` ### 2. Track Business Events, Not HTTP ```typescript // ❌ Don't just track HTTP this.analytics.trackEngagement({ type: 'api_request', action: 'POST /orders', }); // ✅ Track business meaning this.analytics.trackEngagement({ type: 'commerce', action: 'order_created', metadata: { orderId, total, itemCount }, }); ``` ### 3. Fire-and-Forget Never await analytics in request path: ```typescript // ✅ Non-blocking tracking this.analytics.trackEngagement({...}); // No await // ❌ Don't block requests await this.analytics.trackEngagement({...}); ``` ### 4. Handle Failures Silently Analytics should never break your app: ```typescript try { this.analytics.trackEngagement({...}); } catch { // Silent failure - analytics issues shouldn't affect users } ```