12 KiB
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
}