mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-24 04:31:13 -05:00
feat: Implement retry logic and tracking
This commit is contained in:
@@ -284,14 +284,23 @@ export function useModerationActions(config: ModerationActionsConfig): Moderatio
|
||||
}
|
||||
}
|
||||
|
||||
const { data, error, requestId } = await invokeWithTracking(
|
||||
const { data, error, requestId, attempts } = await invokeWithTracking(
|
||||
'process-selective-approval',
|
||||
{
|
||||
itemIds: submissionItems.map((i) => i.id),
|
||||
submissionId: item.id,
|
||||
},
|
||||
config.user?.id
|
||||
config.user?.id,
|
||||
undefined,
|
||||
undefined,
|
||||
30000, // 30s timeout
|
||||
{ maxAttempts: 3, baseDelay: 1500 } // Critical operation - retry config
|
||||
);
|
||||
|
||||
// Log if retries were needed
|
||||
if (attempts && attempts > 1) {
|
||||
logger.log(`Approval succeeded after ${attempts} attempts for ${item.id}`);
|
||||
}
|
||||
|
||||
if (error) throw error;
|
||||
|
||||
@@ -620,14 +629,22 @@ export function useModerationActions(config: ModerationActionsConfig): Moderatio
|
||||
|
||||
failedItemsCount = failedItems.length;
|
||||
|
||||
const { data, error, requestId } = await invokeWithTracking(
|
||||
const { data, error, requestId, attempts } = await invokeWithTracking(
|
||||
'process-selective-approval',
|
||||
{
|
||||
itemIds: failedItems.map((i) => i.id),
|
||||
submissionId: item.id,
|
||||
},
|
||||
config.user?.id
|
||||
config.user?.id,
|
||||
undefined,
|
||||
undefined,
|
||||
30000,
|
||||
{ maxAttempts: 3, baseDelay: 1500 } // Retry for failed items
|
||||
);
|
||||
|
||||
if (attempts && attempts > 1) {
|
||||
logger.log(`Retry succeeded after ${attempts} attempts for ${item.id}`);
|
||||
}
|
||||
|
||||
if (error) throw error;
|
||||
|
||||
@@ -699,16 +716,24 @@ export function useModerationActions(config: ModerationActionsConfig): Moderatio
|
||||
onActionStart(item.id);
|
||||
|
||||
try {
|
||||
// Call edge function for email notification
|
||||
const { error: edgeFunctionError, requestId } = await invokeWithTracking(
|
||||
// Call edge function for email notification with retry
|
||||
const { error: edgeFunctionError, requestId, attempts } = await invokeWithTracking(
|
||||
'send-escalation-notification',
|
||||
{
|
||||
submissionId: item.id,
|
||||
escalationReason: reason,
|
||||
escalatedBy: user.id,
|
||||
},
|
||||
user.id
|
||||
user.id,
|
||||
undefined,
|
||||
undefined,
|
||||
45000, // Longer timeout for email sending
|
||||
{ maxAttempts: 3, baseDelay: 2000 } // Retry for email delivery
|
||||
);
|
||||
|
||||
if (attempts && attempts > 1) {
|
||||
logger.log(`Escalation email sent after ${attempts} attempts`);
|
||||
}
|
||||
|
||||
if (edgeFunctionError) {
|
||||
// Edge function failed - log and show fallback toast
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
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
|
||||
@@ -25,11 +27,32 @@ export async function invokeWithTracking<T = any>(
|
||||
userId?: string,
|
||||
parentRequestId?: string,
|
||||
traceId?: string,
|
||||
timeout: number = 30000 // Default 30s timeout
|
||||
): Promise<{ data: T | null; error: any; requestId: string; duration: number }> {
|
||||
// Create AbortController for timeout
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
||||
timeout: number = 30000, // Default 30s timeout
|
||||
retryOptions?: Partial<RetryOptions> // NEW: Optional retry configuration
|
||||
): Promise<{ data: T | null; error: any; requestId: string; duration: number; attempts?: 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(
|
||||
@@ -41,22 +64,41 @@ export async function invokeWithTracking<T = any>(
|
||||
traceId,
|
||||
},
|
||||
async (context) => {
|
||||
// Include client request ID in payload for correlation
|
||||
const { data, error } = await supabase.functions.invoke<T>(functionName, {
|
||||
body: { ...payload, clientRequestId: context.requestId },
|
||||
signal: controller.signal, // Add abort signal for timeout
|
||||
});
|
||||
|
||||
if (error) throw error;
|
||||
return data;
|
||||
return await withRetry(
|
||||
async () => {
|
||||
attemptCount++;
|
||||
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
||||
|
||||
try {
|
||||
const { data, error } = await supabase.functions.invoke<T>(functionName, {
|
||||
body: { ...payload, clientRequestId: context.requestId },
|
||||
signal: controller.signal,
|
||||
});
|
||||
|
||||
clearTimeout(timeoutId);
|
||||
|
||||
if (error) {
|
||||
// Enhance error with status for retry logic
|
||||
const enhancedError = new Error(error.message || 'Edge function error');
|
||||
(enhancedError as any).status = error.status;
|
||||
throw enhancedError;
|
||||
}
|
||||
|
||||
return data;
|
||||
} catch (error) {
|
||||
clearTimeout(timeoutId);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
effectiveRetryOptions
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
clearTimeout(timeoutId);
|
||||
return { data: result, error: null, requestId, duration };
|
||||
return { data: result, error: null, requestId, duration, attempts: attemptCount };
|
||||
} catch (error: unknown) {
|
||||
clearTimeout(timeoutId);
|
||||
|
||||
// Handle AbortError specifically
|
||||
if (error instanceof Error && error.name === 'AbortError') {
|
||||
return {
|
||||
@@ -67,6 +109,7 @@ export async function invokeWithTracking<T = any>(
|
||||
},
|
||||
requestId: 'timeout',
|
||||
duration: timeout,
|
||||
attempts: attemptCount,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -76,6 +119,7 @@ export async function invokeWithTracking<T = any>(
|
||||
error: { message: errorMessage },
|
||||
requestId: 'unknown',
|
||||
duration: 0,
|
||||
attempts: attemptCount,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -93,6 +137,7 @@ export async function invokeBatchWithTracking<T = any>(
|
||||
operations: Array<{
|
||||
functionName: string;
|
||||
payload: any;
|
||||
retryOptions?: Partial<RetryOptions>;
|
||||
}>,
|
||||
userId?: string
|
||||
): Promise<
|
||||
@@ -102,6 +147,7 @@ export async function invokeBatchWithTracking<T = any>(
|
||||
error: any;
|
||||
requestId: string;
|
||||
duration: number;
|
||||
attempts?: number;
|
||||
}>
|
||||
> {
|
||||
const traceId = crypto.randomUUID();
|
||||
@@ -113,7 +159,9 @@ export async function invokeBatchWithTracking<T = any>(
|
||||
op.payload,
|
||||
userId,
|
||||
undefined,
|
||||
traceId
|
||||
traceId,
|
||||
30000, // default timeout
|
||||
op.retryOptions // Pass through retry options
|
||||
);
|
||||
return { functionName: op.functionName, ...result };
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user