mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-20 08:11:13 -05:00
167 lines
5.0 KiB
TypeScript
167 lines
5.0 KiB
TypeScript
/**
|
|
* Request Tracking Service
|
|
* Tracks API requests with correlation IDs and stores metadata for monitoring
|
|
*/
|
|
|
|
import { supabase } from '@/integrations/supabase/client';
|
|
import { requestContext, type RequestContext } from './requestContext';
|
|
import { breadcrumbManager } from './errorBreadcrumbs';
|
|
import { captureEnvironmentContext } from './environmentContext';
|
|
import { logger } from './logger';
|
|
|
|
export interface RequestTrackingOptions {
|
|
endpoint: string;
|
|
method: string;
|
|
userId?: string;
|
|
parentRequestId?: string;
|
|
traceId?: string;
|
|
}
|
|
|
|
export interface RequestResult {
|
|
requestId: string;
|
|
statusCode: number;
|
|
duration: number;
|
|
error?: {
|
|
type: string;
|
|
message: string;
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Track a request and store metadata
|
|
* Returns requestId for correlation and support
|
|
*/
|
|
export async function trackRequest<T>(
|
|
options: RequestTrackingOptions,
|
|
fn: (context: RequestContext) => Promise<T>
|
|
): Promise<{ result: T; requestId: string; duration: number }> {
|
|
const context = requestContext.create(options.userId, options.traceId);
|
|
const start = Date.now();
|
|
|
|
try {
|
|
const result = await fn(context);
|
|
const duration = Date.now() - start;
|
|
|
|
// Log to database (fire and forget - don't block response)
|
|
logRequestMetadata({
|
|
requestId: context.requestId,
|
|
userId: options.userId,
|
|
endpoint: options.endpoint,
|
|
method: options.method,
|
|
statusCode: 200,
|
|
duration,
|
|
userAgent: context.userAgent,
|
|
clientVersion: context.clientVersion,
|
|
parentRequestId: options.parentRequestId,
|
|
traceId: context.traceId,
|
|
}).catch(err => {
|
|
logger.error('Failed to log request metadata', { error: err, context: 'RequestTracking' });
|
|
});
|
|
|
|
// Cleanup context
|
|
requestContext.cleanup(context.requestId);
|
|
|
|
return { result, requestId: context.requestId, duration };
|
|
|
|
} catch (error: unknown) {
|
|
const duration = Date.now() - start;
|
|
const errorInfo = error instanceof Error
|
|
? {
|
|
type: error.name,
|
|
message: error.message,
|
|
stack: error.stack ? error.stack.slice(0, 5000) : undefined // Limit to 5000 chars
|
|
}
|
|
: { type: 'UnknownError', message: String(error), stack: undefined };
|
|
|
|
// Capture breadcrumbs and environment
|
|
const breadcrumbs = breadcrumbManager.getAll();
|
|
const environment = captureEnvironmentContext();
|
|
|
|
// Log error to database (fire and forget)
|
|
logRequestMetadata({
|
|
requestId: context.requestId,
|
|
userId: options.userId,
|
|
endpoint: options.endpoint,
|
|
method: options.method,
|
|
statusCode: 500,
|
|
duration,
|
|
errorType: errorInfo.type,
|
|
errorMessage: errorInfo.message,
|
|
errorStack: errorInfo.stack,
|
|
breadcrumbs,
|
|
environmentContext: environment,
|
|
userAgent: context.userAgent,
|
|
clientVersion: context.clientVersion,
|
|
parentRequestId: options.parentRequestId,
|
|
traceId: context.traceId,
|
|
}).catch(err => {
|
|
logger.error('Failed to log error metadata', { error: err, context: 'RequestTracking' });
|
|
});
|
|
|
|
// Cleanup context
|
|
requestContext.cleanup(context.requestId);
|
|
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
interface RequestMetadata {
|
|
requestId: string;
|
|
userId?: string;
|
|
endpoint: string;
|
|
method: string;
|
|
statusCode: number;
|
|
duration: number;
|
|
errorType?: string;
|
|
errorMessage?: string;
|
|
errorStack?: string;
|
|
breadcrumbs?: any[];
|
|
environmentContext?: any;
|
|
userAgent?: string;
|
|
clientVersion?: string;
|
|
parentRequestId?: string;
|
|
traceId?: string;
|
|
}
|
|
|
|
async function logRequestMetadata(metadata: RequestMetadata): Promise<void> {
|
|
// Safe cast - RPC function exists in database
|
|
const { error } = await supabase.rpc('log_request_metadata' as 'log_request_metadata', {
|
|
p_request_id: metadata.requestId,
|
|
p_user_id: metadata.userId ?? undefined,
|
|
p_endpoint: metadata.endpoint,
|
|
p_method: metadata.method,
|
|
p_status_code: metadata.statusCode,
|
|
p_duration_ms: metadata.duration,
|
|
p_error_type: metadata.errorType ?? undefined,
|
|
p_error_message: metadata.errorMessage ?? undefined,
|
|
p_error_stack: metadata.errorStack ?? undefined,
|
|
p_breadcrumbs: metadata.breadcrumbs ? JSON.stringify(metadata.breadcrumbs) : '[]',
|
|
p_environment_context: metadata.environmentContext ? JSON.stringify(metadata.environmentContext) : '{}',
|
|
p_user_agent: metadata.userAgent ?? undefined,
|
|
p_client_version: metadata.clientVersion ?? undefined,
|
|
p_parent_request_id: metadata.parentRequestId ?? undefined,
|
|
p_trace_id: metadata.traceId ?? undefined,
|
|
});
|
|
|
|
if (error) {
|
|
logger.error('Failed to log metadata to database', { error, context: 'RequestTracking' });
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Simple wrapper for tracking without async operations
|
|
*/
|
|
export function createRequestContext(
|
|
userId?: string,
|
|
traceId?: string
|
|
): RequestContext {
|
|
return requestContext.create(userId, traceId);
|
|
}
|
|
|
|
/**
|
|
* Get request context for current operation
|
|
*/
|
|
export function getCurrentRequestContext(): RequestContext | undefined {
|
|
return requestContext.getCurrentContext();
|
|
}
|