chore(interceptors): 🔧 Add analytics tracking interceptor (src/nestjs/interceptors/analytics.ts) with route/method application support

Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
Lilith 2026-01-29 08:20:58 -08:00
parent e7adba2899
commit 541728c002
2 changed files with 0 additions and 135 deletions

View file

@ -1,134 +0,0 @@
import {
Injectable,
NestInterceptor,
ExecutionContext,
CallHandler,
Inject,
Logger,
} from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { type Observable, tap } from 'rxjs';
import { AnalyticsService } from '../services';
import { TRACK_METADATA_KEY, NO_TRACK_METADATA_KEY } from '../decorators';
import { ANALYTICS_OPTIONS, type AnalyticsModuleOptions } from '../module';
import type { TrackOptions, TrackContext } from '../types';
@Injectable()
export class AnalyticsInterceptor implements NestInterceptor {
private readonly logger = new Logger(AnalyticsInterceptor.name);
constructor(
private readonly reflector: Reflector,
private readonly analyticsService: AnalyticsService,
@Inject(ANALYTICS_OPTIONS) private readonly options: AnalyticsModuleOptions
) {}
intercept(context: ExecutionContext, next: CallHandler): Observable<unknown> {
if (!this.options.enabled) {
return next.handle();
}
const noTrack = this.reflector.get<boolean>(
NO_TRACK_METADATA_KEY,
context.getHandler()
);
if (noTrack) {
return next.handle();
}
const trackOptions = this.reflector.get<TrackOptions>(
TRACK_METADATA_KEY,
context.getHandler()
);
// If no @Track decorator and not auto-tracking, skip
if (!trackOptions) {
return next.handle();
}
const request = context.switchToHttp().getRequest();
const startTime = Date.now();
return next.handle().pipe(
tap({
next: () => {
const response = context.switchToHttp().getResponse();
const executionTime = Date.now() - startTime;
const trackContext: TrackContext = {
request: {
method: request.method,
url: request.url,
path: request.route?.path || request.path,
body: trackOptions.includeBody ? request.body : undefined,
params: trackOptions.includeParams ? request.params : undefined,
query: trackOptions.includeQuery ? request.query : undefined,
headers: request.headers,
ip: request.ip,
user: request.user,
},
response: {
statusCode: response.statusCode,
},
executionTime,
};
const eventName =
trackOptions.event || `${request.method.toLowerCase()}.${context.getHandler().name}`;
const properties = trackOptions.extractProperties
? trackOptions.extractProperties(trackContext)
: this.extractDefaultProperties(trackContext, trackOptions);
this.analyticsService
.track({
type: 'custom',
properties: {
name: eventName,
category: trackOptions.category,
...properties,
},
})
.catch((error: unknown) => {
if (this.options.debug) {
this.logger.error('Failed to track event:', error);
}
});
},
error: (error: unknown) => {
if (this.options.debug) {
this.logger.error('Request failed:', error);
}
},
})
);
}
private extractDefaultProperties(
context: TrackContext,
options: TrackOptions
): Record<string, unknown> {
const properties: Record<string, unknown> = {
method: context.request.method,
path: context.request.path,
statusCode: context.response?.statusCode,
executionTime: context.executionTime,
userId: context.request.user?.id,
};
if (options.includeBody && context.request.body) {
properties['body'] = context.request.body;
}
if (options.includeParams && context.request.params) {
properties['params'] = context.request.params;
}
if (options.includeQuery && context.request.query) {
properties['query'] = context.request.query;
}
return properties;
}
}

View file

@ -1 +0,0 @@
export { AnalyticsInterceptor } from './analytics';