mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-21 20:51:17 -05:00
Fix composite submission error logging
This commit is contained in:
@@ -8,6 +8,7 @@ import type { CompanyDatabaseRecord, TimelineEventDatabaseRecord } from '@/types
|
|||||||
import { logger } from './logger';
|
import { logger } from './logger';
|
||||||
import { handleError } from './errorHandler';
|
import { handleError } from './errorHandler';
|
||||||
import type { TimelineEventFormData, EntityType } from '@/types/timeline';
|
import type { TimelineEventFormData, EntityType } from '@/types/timeline';
|
||||||
|
import { breadcrumb } from './errorBreadcrumbs';
|
||||||
import {
|
import {
|
||||||
validateParkCreateFields,
|
validateParkCreateFields,
|
||||||
validateRideCreateFields,
|
validateRideCreateFields,
|
||||||
@@ -202,57 +203,106 @@ async function submitCompositeCreation(
|
|||||||
dependencies: CompositeSubmissionDependency[],
|
dependencies: CompositeSubmissionDependency[],
|
||||||
userId: string
|
userId: string
|
||||||
): Promise<{ submitted: boolean; submissionId: string }> {
|
): Promise<{ submitted: boolean; submissionId: string }> {
|
||||||
// Check if user is banned
|
try {
|
||||||
const { data: profile } = await supabase
|
breadcrumb.userAction('Start composite submission', 'submitCompositeCreation', {
|
||||||
.from('profiles')
|
primaryType: primaryEntity.type,
|
||||||
.select('banned')
|
dependencyCount: dependencies.length,
|
||||||
.eq('user_id', userId)
|
userId
|
||||||
.single();
|
});
|
||||||
|
|
||||||
if (profile?.banned) {
|
// Check if user is banned
|
||||||
throw new Error('Account suspended. Contact support for assistance.');
|
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
|
if (error) {
|
||||||
const uploadedEntities = await Promise.all([
|
throw new Error(`Failed to check user status: ${error.message}`);
|
||||||
...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 }
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
return dep;
|
|
||||||
}),
|
if (profile?.banned) {
|
||||||
(async () => {
|
throw new Error('Account suspended. Contact support for assistance.');
|
||||||
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) {
|
||||||
})()
|
throw error instanceof Error ? error : new Error(`User check failed: ${String(error)}`);
|
||||||
]);
|
}
|
||||||
|
|
||||||
const uploadedDependencies = uploadedEntities.slice(0, -1) as CompositeSubmissionDependency[];
|
// Upload all pending images for all entities
|
||||||
const uploadedPrimary = uploadedEntities[uploadedEntities.length - 1] as typeof primaryEntity;
|
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 uploadedEntities = await Promise.all([
|
||||||
const submissionItems: any[] = [];
|
...dependencies.map(async (dep, index) => {
|
||||||
const tempIdMap = new Map<string, number>(); // Maps tempId to order_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
|
const uploadedDependencies = uploadedEntities.slice(0, -1) as CompositeSubmissionDependency[];
|
||||||
let orderIndex = 0;
|
const uploadedPrimary = uploadedEntities[uploadedEntities.length - 1] as typeof primaryEntity;
|
||||||
for (const dep of uploadedDependencies) {
|
|
||||||
|
// 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<string, number>(); // 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;
|
const itemType = dep.type === 'company' ? dep.companyType : dep.type;
|
||||||
tempIdMap.set(dep.tempId, orderIndex);
|
tempIdMap.set(dep.tempId, orderIndex);
|
||||||
|
|
||||||
@@ -370,11 +420,12 @@ async function submitCompositeCreation(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use RPC to create submission with items atomically with retry logic
|
// Use RPC to create submission with items atomically with retry logic
|
||||||
const { withRetry } = await import('./retryHelpers');
|
breadcrumb.apiCall('create_submission_with_items', 'RPC');
|
||||||
const { toast } = await import('@/hooks/use-toast');
|
const { withRetry } = await import('./retryHelpers');
|
||||||
|
const { toast } = await import('@/hooks/use-toast');
|
||||||
const result = await withRetry(
|
|
||||||
|
const result = await withRetry(
|
||||||
async () => {
|
async () => {
|
||||||
const { data, error } = await supabase.rpc('create_submission_with_items', {
|
const { data, error } = await supabase.rpc('create_submission_with_items', {
|
||||||
p_user_id: userId,
|
p_user_id: userId,
|
||||||
@@ -446,24 +497,41 @@ async function submitCompositeCreation(
|
|||||||
return isRetryableError(error);
|
return isRetryableError(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
).catch((error) => {
|
).catch((error) => {
|
||||||
// Final failure - log and throw
|
// Final failure - log and throw
|
||||||
handleError(error, {
|
handleError(error, {
|
||||||
action: 'Composite submission',
|
action: 'Composite submission',
|
||||||
metadata: {
|
metadata: {
|
||||||
primaryType: uploadedPrimary.type,
|
primaryType: uploadedPrimary.type,
|
||||||
dependencyCount: dependencies.length,
|
dependencyCount: dependencies.length,
|
||||||
supabaseCode: (error as any).supabaseCode,
|
supabaseCode: (error as any).supabaseCode,
|
||||||
supabaseDetails: (error as any).supabaseDetails,
|
supabaseDetails: (error as any).supabaseDetails,
|
||||||
supabaseHint: (error as any).supabaseHint,
|
supabaseHint: (error as any).supabaseHint,
|
||||||
retriesExhausted: true
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ export const handleError = (
|
|||||||
let errorMessage: string;
|
let errorMessage: string;
|
||||||
let stack: string | undefined;
|
let stack: string | undefined;
|
||||||
let errorName = 'UnknownError';
|
let errorName = 'UnknownError';
|
||||||
|
let supabaseErrorDetails: Record<string, any> | undefined;
|
||||||
|
|
||||||
if (error instanceof Error) {
|
if (error instanceof Error) {
|
||||||
errorMessage = error instanceof AppError
|
errorMessage = error instanceof AppError
|
||||||
@@ -41,6 +42,15 @@ export const handleError = (
|
|||||||
: error.message;
|
: error.message;
|
||||||
stack = error.stack;
|
stack = error.stack;
|
||||||
errorName = error.name;
|
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') {
|
} else if (error && typeof error === 'object') {
|
||||||
// Handle Supabase errors (plain objects with message/code/details)
|
// Handle Supabase errors (plain objects with message/code/details)
|
||||||
const supabaseError = error as {
|
const supabaseError = error as {
|
||||||
@@ -48,13 +58,24 @@ export const handleError = (
|
|||||||
code?: string;
|
code?: string;
|
||||||
details?: string;
|
details?: string;
|
||||||
hint?: string;
|
hint?: string;
|
||||||
|
stack?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
errorMessage = supabaseError.message || 'An unexpected error occurred';
|
errorMessage = supabaseError.message || 'An unexpected error occurred';
|
||||||
errorName = 'SupabaseError';
|
errorName = 'SupabaseError';
|
||||||
|
|
||||||
// Create synthetic stack trace for Supabase errors to aid debugging
|
// Capture Supabase error details for metadata
|
||||||
if (supabaseError.code || supabaseError.details || supabaseError.hint) {
|
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 = [
|
const stackParts = [
|
||||||
`SupabaseError: ${errorMessage}`,
|
`SupabaseError: ${errorMessage}`,
|
||||||
supabaseError.code ? ` Code: ${supabaseError.code}` : null,
|
supabaseError.code ? ` Code: ${supabaseError.code}` : null,
|
||||||
@@ -68,8 +89,12 @@ export const handleError = (
|
|||||||
}
|
}
|
||||||
} else if (typeof error === 'string') {
|
} else if (typeof error === 'string') {
|
||||||
errorMessage = error;
|
errorMessage = error;
|
||||||
|
// Generate synthetic stack trace for string errors
|
||||||
|
stack = new Error().stack?.replace(/^Error\n/, `StringError: ${error}\n`);
|
||||||
} else {
|
} else {
|
||||||
errorMessage = 'An unexpected error occurred';
|
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
|
// Log to console/monitoring with enhanced debugging
|
||||||
@@ -84,6 +109,7 @@ export const handleError = (
|
|||||||
errorConstructor: error?.constructor?.name,
|
errorConstructor: error?.constructor?.name,
|
||||||
hasStack: !!stack,
|
hasStack: !!stack,
|
||||||
isSyntheticStack: !!(error && typeof error === 'object' && !(error instanceof Error) && stack),
|
isSyntheticStack: !!(error && typeof error === 'object' && !(error instanceof Error) && stack),
|
||||||
|
supabaseError: supabaseErrorDetails,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Additional debug logging when stack is missing
|
// Additional debug logging when stack is missing
|
||||||
@@ -114,11 +140,13 @@ export const handleError = (
|
|||||||
p_error_stack: stack,
|
p_error_stack: stack,
|
||||||
p_user_agent: navigator.userAgent,
|
p_user_agent: navigator.userAgent,
|
||||||
p_breadcrumbs: JSON.stringify({
|
p_breadcrumbs: JSON.stringify({
|
||||||
...breadcrumbs,
|
breadcrumbs,
|
||||||
isRetry: context.metadata?.isRetry || false,
|
isRetry: context.metadata?.isRetry || false,
|
||||||
attempt: context.metadata?.attempt,
|
attempt: context.metadata?.attempt,
|
||||||
retriesExhausted: context.metadata?.retriesExhausted || false,
|
retriesExhausted: context.metadata?.retriesExhausted || false,
|
||||||
circuitBreakerState: context.metadata?.circuitState,
|
circuitBreakerState: context.metadata?.circuitState,
|
||||||
|
supabaseError: supabaseErrorDetails,
|
||||||
|
metadata: context.metadata
|
||||||
}),
|
}),
|
||||||
p_timezone: envContext.timezone,
|
p_timezone: envContext.timezone,
|
||||||
p_referrer: document.referrer || undefined,
|
p_referrer: document.referrer || undefined,
|
||||||
|
|||||||
Reference in New Issue
Block a user