255 lines
6.6 KiB
TypeScript
Executable file
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();
|
|
};
|
|
}
|