/** * Edge Function Request Tracking Wrapper * * Wraps Supabase function invocations with request tracking for debugging and monitoring. * Provides correlation IDs for tracing requests across the system. */ import { supabase } from '@/lib/supabaseClient'; import { trackRequest } from './requestTracking'; import { getErrorMessage } from './errorHandler'; import { withRetry, isRetryableError, type RetryOptions } from './retryHelpers'; import { breadcrumb } from './errorBreadcrumbs'; /** * Invoke a Supabase edge function with request tracking * * @param functionName - Name of the edge function to invoke * @param payload - Request payload * @param userId - User ID for tracking (optional) * @param parentRequestId - Parent request ID for chaining (optional) * @param traceId - Trace ID for distributed tracing (optional) * @param timeout - Request timeout in milliseconds (default: 30000) * @param retryOptions - Optional retry configuration * @param customHeaders - Custom headers to include in the request (e.g., X-Idempotency-Key) * @returns Response data with requestId, status, and tracking info */ export async function invokeWithTracking( functionName: string, payload: any = {}, userId?: string, parentRequestId?: string, traceId?: string, timeout: number = 30000, retryOptions?: Partial, customHeaders?: Record ): Promise<{ data: T | null; error: any; requestId: string; duration: number; attempts?: number; status?: number }> { // Configure retry options with defaults const effectiveRetryOptions: RetryOptions = { maxAttempts: retryOptions?.maxAttempts ?? 3, baseDelay: retryOptions?.baseDelay ?? 1000, maxDelay: retryOptions?.maxDelay ?? 10000, backoffMultiplier: retryOptions?.backoffMultiplier ?? 2, jitter: true, shouldRetry: isRetryableError, onRetry: (attempt, error, delay) => { // Log retry attempt to breadcrumbs breadcrumb.apiCall( `/functions/${functionName}`, 'POST', undefined // status unknown during retry ); console.info(`Retrying ${functionName} (attempt ${attempt}) after ${delay}ms:`, getErrorMessage(error) ); }, }; let attemptCount = 0; try { const { result, requestId, duration } = await trackRequest( { endpoint: `/functions/${functionName}`, method: 'POST', userId, parentRequestId, traceId, }, async (context) => { return await withRetry( async () => { attemptCount++; const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), timeout); try { const { data, error } = await supabase.functions.invoke(functionName, { body: { ...payload, clientRequestId: context.requestId }, signal: controller.signal, headers: customHeaders, }); clearTimeout(timeoutId); if (error) { // Enhance error with status and context for retry logic const enhancedError = new Error(error.message || 'Edge function error'); (enhancedError as any).status = error.status; (enhancedError as any).context = error.context; throw enhancedError; } return data; } catch (error) { clearTimeout(timeoutId); throw error; } }, effectiveRetryOptions ); } ); return { data: result, error: null, requestId, duration, attempts: attemptCount, status: 200 }; } catch (error: unknown) { // Handle AbortError specifically if (error instanceof Error && error.name === 'AbortError') { return { data: null, error: { message: `Request timeout: ${functionName} took longer than ${timeout}ms to respond`, code: 'TIMEOUT', }, requestId: 'timeout', duration: timeout, attempts: attemptCount, status: 408, }; } const errorMessage = getErrorMessage(error); return { data: null, error: { message: errorMessage, status: (error as any)?.status }, requestId: 'unknown', duration: 0, attempts: attemptCount, status: (error as any)?.status, }; } } /** * Invoke multiple edge functions in parallel with batch tracking * * Uses a shared trace ID to correlate all operations. * * @param operations - Array of function invocation configurations * @param userId - User ID for tracking * @returns Array of results with their request IDs */ export async function invokeBatchWithTracking( operations: Array<{ functionName: string; payload: any; retryOptions?: Partial; }>, userId?: string ): Promise< Array<{ functionName: string; data: T | null; error: any; requestId: string; duration: number; attempts?: number; status?: number; }> > { const traceId = crypto.randomUUID(); const results = await Promise.allSettled( operations.map(async (op) => { const result = await invokeWithTracking( op.functionName, op.payload, userId, undefined, traceId, 30000, op.retryOptions ); return { functionName: op.functionName, ...result }; }) ); return results.map((result, index) => { if (result.status === 'fulfilled') { return result.value; } else { return { functionName: operations[index].functionName, data: null, error: { message: result.reason?.message || 'Unknown error' }, requestId: 'unknown', duration: 0, }; } }); }