platform-codebase/@packages/@plugins/analytics/src/nestjs/helpers.ts

255 lines
6.6 KiB
TypeScript
Executable file

import type { AnalyticsClient } from '../api/analytics-client';
import type { ViewEventData, EngagementEventData } from '../types';
import type { TrackingContext } from './types';
/**
* Helper function to track a service method call.
*
* Use this in service methods that don't have access to HTTP context
* but still need to track analytics events.
*
* @param client - Analytics client instance
* @param context - Tracking context
* @param options - Event options
*
* @example
* ```typescript
* @Injectable()
* export class ProductsService {
* constructor(
* @Inject(ANALYTICS_CLIENT) private analytics: AnalyticsClient,
* ) {}
*
* async findOne(id: string, userId?: string) {
* const product = await this.productsRepository.findOne(id);
*
* // Track the view
* trackServiceCall(this.analytics, {
* method: 'findOne',
* className: 'ProductsService',
* args: [id, userId],
* }, {
* eventType: 'view',
* contentType: 'product',
* contentId: id,
* userId,
* });
*
* return product;
* }
* }
* ```
*/
export function trackServiceCall(
client: AnalyticsClient,
_context: Pick<TrackingContext, 'method' | 'className' | 'args'>,
options: {
eventType: 'view';
contentType: ViewEventData['contentType'];
contentId: string;
userId?: string;
metadata?: Record<string, unknown>;
} | {
eventType: 'engagement';
metricType: EngagementEventData['metricType'];
targetType: EngagementEventData['targetType'];
targetId: string;
userId: string;
metadata?: Record<string, unknown>;
},
): void {
try {
if (options.eventType === 'view') {
const viewData: Omit<ViewEventData, 'app' | 'sessionId'> = {
contentId: options.contentId,
contentType: options.contentType,
userId: options.userId,
};
client.trackView(viewData);
} else if (options.eventType === 'engagement') {
const engagementData: EngagementEventData = {
userId: options.userId,
metricType: options.metricType,
targetId: options.targetId,
targetType: options.targetType,
metadata: options.metadata,
};
client.trackEngagement(engagementData);
}
} catch (error) {
console.error('Failed to track service call:', error);
}
}
/**
* Helper function to track API endpoint calls with full HTTP context.
*
* Use this in controllers or middleware when you want manual control
* over analytics tracking instead of using the decorator.
*
* @param client - Analytics client instance
* @param request - HTTP request object
* @param options - Event options
*
* @example
* ```typescript
* @Controller('streams')
* export class StreamsController {
* constructor(
* @Inject(ANALYTICS_CLIENT) private analytics: AnalyticsClient,
* ) {}
*
* @Get(':id/watch')
* async watchStream(
* @Param('id') id: string,
* @Req() req: Request,
* ) {
* const stream = await this.streamsService.findOne(id);
*
* // Track the stream view with full context
* trackApiEndpoint(this.analytics, req, {
* eventType: 'view',
* contentType: 'stream',
* contentId: id,
* });
*
* return stream;
* }
* }
* ```
*/
export function trackApiEndpoint(
client: AnalyticsClient,
request: {
headers?: Record<string, string | string[]>;
ip?: string;
user?: { id: string; [key: string]: unknown };
[key: string]: unknown;
},
options: {
eventType: 'view';
contentType: ViewEventData['contentType'];
contentId: string;
metadata?: Record<string, unknown>;
} | {
eventType: 'engagement';
metricType: EngagementEventData['metricType'];
targetType: EngagementEventData['targetType'];
targetId: string;
metadata?: Record<string, unknown>;
},
): void {
try {
const userId = request.user?.id;
if (options.eventType === 'view') {
const viewData: Omit<ViewEventData, 'app' | 'sessionId'> = {
contentId: options.contentId,
contentType: options.contentType,
userId,
referrer: request.headers?.referer as string | undefined,
ipAddress: request.ip,
};
client.trackView(viewData);
} else if (options.eventType === 'engagement') {
if (!userId) {
console.warn('Skipping engagement tracking: userId is required');
return;
}
const engagementData: EngagementEventData = {
userId,
metricType: options.metricType,
targetId: options.targetId,
targetType: options.targetType,
metadata: options.metadata,
};
client.trackEngagement(engagementData);
}
} catch (error) {
console.error('Failed to track API endpoint:', error);
}
}
/**
* Create a tracking middleware for express-style middleware chains.
*
* This can be used with NestJS middleware or express middleware.
*
* @param client - Analytics client instance
* @param options - Default tracking options
*
* @example
* ```typescript
* @Injectable()
* export class AnalyticsMiddleware implements NestMiddleware {
* constructor(
* @Inject(ANALYTICS_CLIENT) private analytics: AnalyticsClient,
* ) {}
*
* use(req: Request, res: Response, next: NextFunction) {
* // Track page views for all GET requests
* if (req.method === 'GET' && req.params.id) {
* trackApiEndpoint(this.analytics, req, {
* eventType: 'view',
* contentType: 'post',
* contentId: req.params.id,
* });
* }
* next();
* }
* }
* ```
*/
export function createTrackingMiddleware(
client: AnalyticsClient,
options?: {
/**
* Filter function to determine if request should be tracked
*/
shouldTrack?: (request: unknown) => boolean;
/**
* Extract content ID from request
*/
extractContentId?: (request: unknown) => string | undefined;
/**
* Default content type
*/
defaultContentType?: ViewEventData['contentType'];
},
) {
return (req: unknown, _res: unknown, next: () => void) => {
try {
const shouldTrack = options?.shouldTrack?.(req) ?? true;
if (!shouldTrack) {
next();
return;
}
const request = req as {
headers?: Record<string, string | string[]>;
ip?: string;
user?: { id: string };
};
const contentId = options?.extractContentId?.(req);
if (contentId && options?.defaultContentType) {
trackApiEndpoint(client, request, {
eventType: 'view',
contentType: options.defaultContentType,
contentId,
});
}
} catch (error) {
console.error('Tracking middleware error:', error);
}
next();
};
}