analytics/docs/nestjs-integration.md
Lilith dc5329e885 chore(docs): 📝 Update documentation files in /docs directory (README, guides, or API references)
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
2026-01-29 08:20:58 -08:00

12 KiB

NestJS Integration Guide

Server-side analytics tracking for NestJS applications.

Installation

npm install @analytics/client

Module Setup

1. Create Analytics Module

// 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<AnalyticsModuleOptions>;
  }): 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

// 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.

// 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<any> {
    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

// 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.

// 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<string, unknown>;
  extractMetadata?: (context: {
    request: any;
    response: any;
    result: any;
  }) => Record<string, unknown>;
}

export const TrackAnalytics = (options: TrackAnalyticsOptions) =>
  SetMetadata(TRACK_ANALYTICS_KEY, options);

Usage

// 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.

// 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<Order> {
    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<Order> {
    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.

// 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.

// 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:

// Frontend
fetch('/api/orders', {
  headers: {
    'x-session-id': analytics.getSessionId(),
  },
});

2. Track Business Events, Not HTTP

// ❌ 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:

// ✅ 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:

try {
  this.analytics.trackEngagement({...});
} catch {
  // Silent failure - analytics issues shouldn't affect users
}