mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-21 11:11:12 -05:00
Refactor code structure and remove redundant changes
This commit is contained in:
335
src-old/lib/errorHandler.ts
Normal file
335
src-old/lib/errorHandler.ts
Normal file
@@ -0,0 +1,335 @@
|
||||
import { toast } from 'sonner';
|
||||
import { logger } from './logger';
|
||||
import { supabase } from '@/integrations/supabase/client';
|
||||
import { breadcrumbManager } from './errorBreadcrumbs';
|
||||
import { captureEnvironmentContext } from './environmentContext';
|
||||
|
||||
export type ErrorContext = {
|
||||
action: string;
|
||||
userId?: string;
|
||||
metadata?: Record<string, unknown>;
|
||||
duration?: number; // Optional: milliseconds the operation took
|
||||
};
|
||||
|
||||
export class AppError extends Error {
|
||||
constructor(
|
||||
message: string,
|
||||
public code: string,
|
||||
public userMessage?: string
|
||||
) {
|
||||
super(message);
|
||||
this.name = 'AppError';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if error is a Supabase connection/API error
|
||||
*/
|
||||
export function isSupabaseConnectionError(error: unknown): boolean {
|
||||
if (error && typeof error === 'object') {
|
||||
const supabaseError = error as { code?: string; status?: number; message?: string };
|
||||
|
||||
// Connection timeout errors
|
||||
if (supabaseError.code === 'PGRST301') return true; // Timeout
|
||||
if (supabaseError.code === 'PGRST000') return true; // Connection error
|
||||
|
||||
// 5xx server errors
|
||||
if (supabaseError.status && supabaseError.status >= 500) return true;
|
||||
|
||||
// Database connection errors (08xxx codes)
|
||||
if (supabaseError.code?.startsWith('08')) return true;
|
||||
}
|
||||
|
||||
// Network fetch errors
|
||||
if (error instanceof TypeError) {
|
||||
const message = error.message.toLowerCase();
|
||||
if (message.includes('fetch') || message.includes('network') || message.includes('failed to fetch')) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
export const handleError = (
|
||||
error: unknown,
|
||||
context: ErrorContext
|
||||
): string => {
|
||||
// Generate or use existing error ID
|
||||
const errorId = (context.metadata?.requestId as string) || crypto.randomUUID();
|
||||
const shortErrorId = errorId.slice(0, 8);
|
||||
|
||||
// Check if this is a connection error and dispatch event
|
||||
if (isSupabaseConnectionError(error)) {
|
||||
window.dispatchEvent(new CustomEvent('api-connectivity-down'));
|
||||
}
|
||||
|
||||
// Enhanced error message and stack extraction
|
||||
let errorMessage: string;
|
||||
let stack: string | undefined;
|
||||
let errorName = 'UnknownError';
|
||||
let supabaseErrorDetails: Record<string, any> | undefined;
|
||||
|
||||
if (error instanceof Error) {
|
||||
errorMessage = error instanceof AppError
|
||||
? error.userMessage || error.message
|
||||
: 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 {
|
||||
message?: string;
|
||||
code?: string;
|
||||
details?: string;
|
||||
hint?: string;
|
||||
stack?: string;
|
||||
};
|
||||
|
||||
errorMessage = supabaseError.message || 'An unexpected error occurred';
|
||||
errorName = 'SupabaseError';
|
||||
|
||||
// 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,
|
||||
supabaseError.details ? ` Details: ${supabaseError.details}` : null,
|
||||
supabaseError.hint ? ` Hint: ${supabaseError.hint}` : null,
|
||||
` at ${context.action}`,
|
||||
` Reference ID: ${errorId}`
|
||||
].filter(Boolean);
|
||||
|
||||
stack = stackParts.join('\n');
|
||||
}
|
||||
} 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
|
||||
|
||||
logger.error('Error occurred', {
|
||||
...context,
|
||||
error: errorMessage,
|
||||
stack,
|
||||
errorId,
|
||||
errorName,
|
||||
errorType: typeof error,
|
||||
errorConstructor: error?.constructor?.name,
|
||||
hasStack: !!stack,
|
||||
isSyntheticStack: !!(error && typeof error === 'object' && !(error instanceof Error) && stack),
|
||||
supabaseError: supabaseErrorDetails,
|
||||
});
|
||||
|
||||
// Additional debug logging when stack is missing
|
||||
if (!stack) {
|
||||
console.error('[handleError] Error without stack trace:', {
|
||||
type: typeof error,
|
||||
constructor: error?.constructor?.name,
|
||||
error: error,
|
||||
context,
|
||||
errorId
|
||||
});
|
||||
}
|
||||
|
||||
// Log to database with breadcrumbs (non-blocking)
|
||||
try {
|
||||
const envContext = captureEnvironmentContext();
|
||||
const breadcrumbs = breadcrumbManager.getAll();
|
||||
|
||||
// Fire-and-forget database logging
|
||||
supabase.rpc('log_request_metadata', {
|
||||
p_request_id: errorId,
|
||||
p_user_id: context.userId || undefined,
|
||||
p_endpoint: context.action,
|
||||
p_method: 'ERROR',
|
||||
p_status_code: 500,
|
||||
p_error_type: errorName,
|
||||
p_error_message: errorMessage,
|
||||
p_error_stack: stack,
|
||||
p_user_agent: navigator.userAgent,
|
||||
p_breadcrumbs: JSON.stringify({
|
||||
breadcrumbs,
|
||||
isRetry: context.metadata?.isRetry || false,
|
||||
attempt: context.metadata?.attempt,
|
||||
retriesExhausted: context.metadata?.retriesExhausted || false,
|
||||
supabaseError: supabaseErrorDetails,
|
||||
metadata: context.metadata
|
||||
}),
|
||||
p_timezone: envContext.timezone,
|
||||
p_referrer: document.referrer || undefined,
|
||||
p_duration_ms: context.duration,
|
||||
}).then(({ error: dbError }) => {
|
||||
if (dbError) {
|
||||
logger.error('Failed to log error to database', { dbError });
|
||||
}
|
||||
});
|
||||
} catch (logError) {
|
||||
logger.error('Failed to capture error context', { logError });
|
||||
}
|
||||
|
||||
// Show user-friendly toast with error ID (skip for retry attempts)
|
||||
const isRetry = context.metadata?.isRetry === true || context.metadata?.attempt;
|
||||
if (!isRetry) {
|
||||
toast.error(context.action, {
|
||||
description: `${errorMessage}\n\nReference ID: ${shortErrorId}`,
|
||||
duration: 5000,
|
||||
});
|
||||
}
|
||||
|
||||
return errorId;
|
||||
};
|
||||
|
||||
export const handleSuccess = (
|
||||
title: string,
|
||||
description?: string
|
||||
): void => {
|
||||
toast.success(title, {
|
||||
description,
|
||||
duration: 3000
|
||||
});
|
||||
};
|
||||
|
||||
export const handleInfo = (
|
||||
title: string,
|
||||
description?: string
|
||||
): void => {
|
||||
toast.info(title, {
|
||||
description,
|
||||
duration: 4000
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle non-critical errors (background failures) that should be logged
|
||||
* to the database WITHOUT showing user toasts
|
||||
* Use this for fire-and-forget operations where user shouldn't be interrupted
|
||||
*/
|
||||
export const handleNonCriticalError = (
|
||||
error: unknown,
|
||||
context: ErrorContext
|
||||
): string => {
|
||||
const errorId = crypto.randomUUID();
|
||||
const shortErrorId = errorId.slice(0, 8);
|
||||
|
||||
const errorMessage = error instanceof AppError
|
||||
? error.userMessage || error.message
|
||||
: error instanceof Error
|
||||
? error.message
|
||||
: 'An unexpected error occurred';
|
||||
|
||||
// Log to console/monitoring (same as handleError)
|
||||
logger.error('Non-critical error occurred', {
|
||||
...context,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
errorId,
|
||||
severity: 'low',
|
||||
});
|
||||
|
||||
// Log to database with breadcrumbs (non-blocking, fire-and-forget)
|
||||
try {
|
||||
const envContext = captureEnvironmentContext();
|
||||
const breadcrumbs = breadcrumbManager.getAll();
|
||||
|
||||
supabase.rpc('log_request_metadata', {
|
||||
p_request_id: errorId,
|
||||
p_user_id: context.userId || undefined,
|
||||
p_endpoint: context.action,
|
||||
p_method: 'NON_CRITICAL_ERROR',
|
||||
p_status_code: 500,
|
||||
p_error_type: error instanceof Error ? error.name : 'UnknownError',
|
||||
p_error_message: errorMessage,
|
||||
p_error_stack: error instanceof Error ? error.stack : undefined,
|
||||
p_user_agent: navigator.userAgent,
|
||||
p_breadcrumbs: JSON.stringify({
|
||||
breadcrumbs,
|
||||
metadata: context.metadata // Include metadata for debugging
|
||||
}),
|
||||
p_timezone: envContext.timezone,
|
||||
p_referrer: document.referrer || undefined,
|
||||
p_duration_ms: context.duration,
|
||||
}).then(({ error: dbError }) => {
|
||||
if (dbError) {
|
||||
logger.error('Failed to log non-critical error to database', { dbError });
|
||||
}
|
||||
});
|
||||
} catch (logError) {
|
||||
logger.error('Failed to capture non-critical error context', { logError });
|
||||
}
|
||||
|
||||
// NO TOAST - This is the key difference from handleError()
|
||||
return errorId;
|
||||
};
|
||||
|
||||
/**
|
||||
* Type-safe error message extraction utility
|
||||
* Use this instead of `error: any` in catch blocks
|
||||
*/
|
||||
export function getErrorMessage(error: unknown): string {
|
||||
if (error instanceof Error) {
|
||||
return error.message;
|
||||
}
|
||||
if (typeof error === 'string') {
|
||||
return error;
|
||||
}
|
||||
if (error && typeof error === 'object' && 'message' in error) {
|
||||
return String(error.message);
|
||||
}
|
||||
return 'An unexpected error occurred';
|
||||
}
|
||||
|
||||
/**
|
||||
* Type guard to check if error has a code property
|
||||
*/
|
||||
export function hasErrorCode(error: unknown): error is { code: string } {
|
||||
return (
|
||||
error !== null &&
|
||||
typeof error === 'object' &&
|
||||
'code' in error &&
|
||||
typeof (error as { code: unknown }).code === 'string'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to wrap async operations with automatic duration tracking
|
||||
* Use this for operations where you want to track how long they took before failing
|
||||
*/
|
||||
export async function withErrorTiming<T>(
|
||||
fn: () => Promise<T>,
|
||||
errorContext: Omit<ErrorContext, 'duration'>
|
||||
): Promise<T> {
|
||||
const start = performance.now();
|
||||
try {
|
||||
return await fn();
|
||||
} catch (error) {
|
||||
const duration = Math.round(performance.now() - start);
|
||||
handleError(error, { ...errorContext, duration });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user