diff --git a/src/lib/entitySubmissionHelpers.ts b/src/lib/entitySubmissionHelpers.ts index 9cb1c597..8989393c 100644 --- a/src/lib/entitySubmissionHelpers.ts +++ b/src/lib/entitySubmissionHelpers.ts @@ -8,6 +8,7 @@ import type { CompanyDatabaseRecord, TimelineEventDatabaseRecord } from '@/types import { logger } from './logger'; import { handleError } from './errorHandler'; import type { TimelineEventFormData, EntityType } from '@/types/timeline'; +import { breadcrumb } from './errorBreadcrumbs'; import { validateParkCreateFields, validateRideCreateFields, @@ -202,57 +203,106 @@ async function submitCompositeCreation( dependencies: CompositeSubmissionDependency[], userId: string ): Promise<{ submitted: boolean; submissionId: string }> { - // Check if user is banned - const { data: profile } = await supabase - .from('profiles') - .select('banned') - .eq('user_id', userId) - .single(); + try { + breadcrumb.userAction('Start composite submission', 'submitCompositeCreation', { + primaryType: primaryEntity.type, + dependencyCount: dependencies.length, + userId + }); - if (profile?.banned) { - throw new Error('Account suspended. Contact support for assistance.'); - } + // Check if user is banned + breadcrumb.apiCall('profiles', 'SELECT'); + try { + const { data: profile, error } = await supabase + .from('profiles') + .select('banned') + .eq('user_id', userId) + .single(); - // Upload all pending images for all entities - const uploadedEntities = await Promise.all([ - ...dependencies.map(async (dep) => { - if (dep.data.images?.uploaded && dep.data.images.uploaded.length > 0) { - const uploadedImages = await uploadPendingImages(dep.data.images.uploaded); - return { - ...dep, - data: { - ...dep.data, - images: { ...dep.data.images, uploaded: uploadedImages } - } - }; + if (error) { + throw new Error(`Failed to check user status: ${error.message}`); } - return dep; - }), - (async () => { - if (primaryEntity.data.images?.uploaded && primaryEntity.data.images.uploaded.length > 0) { - const uploadedImages = await uploadPendingImages(primaryEntity.data.images.uploaded); - return { - ...primaryEntity, - data: { - ...primaryEntity.data, - images: { ...primaryEntity.data.images, uploaded: uploadedImages } - } - }; + + if (profile?.banned) { + throw new Error('Account suspended. Contact support for assistance.'); } - return primaryEntity; - })() - ]); + } catch (error) { + throw error instanceof Error ? error : new Error(`User check failed: ${String(error)}`); + } - const uploadedDependencies = uploadedEntities.slice(0, -1) as CompositeSubmissionDependency[]; - const uploadedPrimary = uploadedEntities[uploadedEntities.length - 1] as typeof primaryEntity; + // Upload all pending images for all entities + breadcrumb.userAction('Upload images', 'submitCompositeCreation', { + totalImages: dependencies.reduce((sum, dep) => sum + (dep.data.images?.uploaded?.length || 0), 0) + + (primaryEntity.data.images?.uploaded?.length || 0) + }); - // Build submission items array with dependencies first - const submissionItems: any[] = []; - const tempIdMap = new Map(); // Maps tempId to order_index + const uploadedEntities = await Promise.all([ + ...dependencies.map(async (dep, index) => { + try { + if (dep.data.images?.uploaded && dep.data.images.uploaded.length > 0) { + const uploadedImages = await uploadPendingImages(dep.data.images.uploaded); + return { + ...dep, + data: { + ...dep.data, + images: { ...dep.data.images, uploaded: uploadedImages } + } + }; + } + return dep; + } catch (error) { + const errorMsg = error instanceof Error ? error.message : String(error); + throw new Error( + `Failed to upload images for ${dep.type} "${dep.data.name || 'unnamed'}": ${errorMsg}` + ); + } + }), + (async () => { + try { + if (primaryEntity.data.images?.uploaded && primaryEntity.data.images.uploaded.length > 0) { + const uploadedImages = await uploadPendingImages(primaryEntity.data.images.uploaded); + return { + ...primaryEntity, + data: { + ...primaryEntity.data, + images: { ...primaryEntity.data.images, uploaded: uploadedImages } + } + }; + } + return primaryEntity; + } catch (error) { + const errorMsg = error instanceof Error ? error.message : String(error); + throw new Error( + `Failed to upload images for ${primaryEntity.type} "${primaryEntity.data.name || 'unnamed'}": ${errorMsg}` + ); + } + })() + ]); - // Add dependency items (companies, models) first - let orderIndex = 0; - for (const dep of uploadedDependencies) { + const uploadedDependencies = uploadedEntities.slice(0, -1) as CompositeSubmissionDependency[]; + const uploadedPrimary = uploadedEntities[uploadedEntities.length - 1] as typeof primaryEntity; + + // Validate dependencies structure + breadcrumb.stateChange('Validating dependencies', { + dependencyCount: uploadedDependencies.length + }); + + for (const dep of uploadedDependencies) { + if (!dep.type) throw new Error('Dependency missing type'); + if (!dep.tempId) throw new Error('Dependency missing tempId'); + if (!dep.data) throw new Error('Dependency missing data'); + if (dep.type === 'company' && !dep.companyType) { + throw new Error(`Company dependency "${dep.data.name || 'unnamed'}" missing companyType`); + } + } + + // Build submission items array with dependencies first + const submissionItems: any[] = []; + const tempIdMap = new Map(); // Maps tempId to order_index + + // Add dependency items (companies, models) first + let orderIndex = 0; + for (const dep of uploadedDependencies) { const itemType = dep.type === 'company' ? dep.companyType : dep.type; tempIdMap.set(dep.tempId, orderIndex); @@ -370,11 +420,12 @@ async function submitCompositeCreation( } } - // Use RPC to create submission with items atomically with retry logic - const { withRetry } = await import('./retryHelpers'); - const { toast } = await import('@/hooks/use-toast'); - - const result = await withRetry( + // Use RPC to create submission with items atomically with retry logic + breadcrumb.apiCall('create_submission_with_items', 'RPC'); + const { withRetry } = await import('./retryHelpers'); + const { toast } = await import('@/hooks/use-toast'); + + const result = await withRetry( async () => { const { data, error } = await supabase.rpc('create_submission_with_items', { p_user_id: userId, @@ -446,24 +497,41 @@ async function submitCompositeCreation( return isRetryableError(error); } } - ).catch((error) => { - // Final failure - log and throw - handleError(error, { - action: 'Composite submission', - metadata: { - primaryType: uploadedPrimary.type, - dependencyCount: dependencies.length, - supabaseCode: (error as any).supabaseCode, - supabaseDetails: (error as any).supabaseDetails, - supabaseHint: (error as any).supabaseHint, - retriesExhausted: true - }, + ).catch((error) => { + // Final failure - log and throw + handleError(error, { + action: 'Composite submission', + metadata: { + primaryType: uploadedPrimary.type, + dependencyCount: dependencies.length, + supabaseCode: (error as any).supabaseCode, + supabaseDetails: (error as any).supabaseDetails, + supabaseHint: (error as any).supabaseHint, + retriesExhausted: true + }, + }); + + throw error; }); - - throw error; - }); - return { submitted: true, submissionId: result }; + breadcrumb.stateChange('Composite submission successful', { + submissionId: result + }); + + return { submitted: true, submissionId: result }; + } catch (error) { + // Ensure error is always an Error instance with context + const enrichedError = error instanceof Error + ? error + : new Error(`Composite submission failed: ${String(error)}`); + + // Attach metadata for better debugging + (enrichedError as any).originalError = error; + (enrichedError as any).primaryType = primaryEntity?.type; + (enrichedError as any).dependencyCount = dependencies?.length; + + throw enrichedError; + } } /** diff --git a/src/lib/errorHandler.ts b/src/lib/errorHandler.ts index 8154c46a..d24f1d51 100644 --- a/src/lib/errorHandler.ts +++ b/src/lib/errorHandler.ts @@ -34,6 +34,7 @@ export const handleError = ( let errorMessage: string; let stack: string | undefined; let errorName = 'UnknownError'; + let supabaseErrorDetails: Record | undefined; if (error instanceof Error) { errorMessage = error instanceof AppError @@ -41,6 +42,15 @@ export const handleError = ( : error.message; stack = error.stack; errorName = error.name; + + // Check if Error instance has attached Supabase metadata + if ((error as any).supabaseCode) { + supabaseErrorDetails = { + code: (error as any).supabaseCode, + details: (error as any).supabaseDetails, + hint: (error as any).supabaseHint + }; + } } else if (error && typeof error === 'object') { // Handle Supabase errors (plain objects with message/code/details) const supabaseError = error as { @@ -48,13 +58,24 @@ export const handleError = ( code?: string; details?: string; hint?: string; + stack?: string; }; errorMessage = supabaseError.message || 'An unexpected error occurred'; errorName = 'SupabaseError'; - // Create synthetic stack trace for Supabase errors to aid debugging - if (supabaseError.code || supabaseError.details || supabaseError.hint) { + // Capture Supabase error details for metadata + supabaseErrorDetails = { + code: supabaseError.code, + details: supabaseError.details, + hint: supabaseError.hint + }; + + // Try to extract stack from object + if (supabaseError.stack && typeof supabaseError.stack === 'string') { + stack = supabaseError.stack; + } else if (supabaseError.code || supabaseError.details || supabaseError.hint) { + // Create synthetic stack trace for Supabase errors to aid debugging const stackParts = [ `SupabaseError: ${errorMessage}`, supabaseError.code ? ` Code: ${supabaseError.code}` : null, @@ -68,8 +89,12 @@ export const handleError = ( } } else if (typeof error === 'string') { errorMessage = error; + // Generate synthetic stack trace for string errors + stack = new Error().stack?.replace(/^Error\n/, `StringError: ${error}\n`); } else { errorMessage = 'An unexpected error occurred'; + // Generate synthetic stack trace for unknown error types + stack = new Error().stack?.replace(/^Error\n/, `UnknownError: ${String(error)}\n`); } // Log to console/monitoring with enhanced debugging @@ -84,6 +109,7 @@ export const handleError = ( errorConstructor: error?.constructor?.name, hasStack: !!stack, isSyntheticStack: !!(error && typeof error === 'object' && !(error instanceof Error) && stack), + supabaseError: supabaseErrorDetails, }); // Additional debug logging when stack is missing @@ -114,11 +140,13 @@ export const handleError = ( p_error_stack: stack, p_user_agent: navigator.userAgent, p_breadcrumbs: JSON.stringify({ - ...breadcrumbs, + breadcrumbs, isRetry: context.metadata?.isRetry || false, attempt: context.metadata?.attempt, retriesExhausted: context.metadata?.retriesExhausted || false, circuitBreakerState: context.metadata?.circuitState, + supabaseError: supabaseErrorDetails, + metadata: context.metadata }), p_timezone: envContext.timezone, p_referrer: document.referrer || undefined,