500 lines
12 KiB
Markdown
500 lines
12 KiB
Markdown
# 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<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
|
|
|
|
```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<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
|
|
|
|
```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<string, unknown>;
|
|
extractMetadata?: (context: {
|
|
request: any;
|
|
response: any;
|
|
result: any;
|
|
}) => Record<string, unknown>;
|
|
}
|
|
|
|
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<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.
|
|
|
|
```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
|
|
}
|
|
```
|