mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-21 11:11:12 -05:00
205 lines
5.7 KiB
TypeScript
205 lines
5.7 KiB
TypeScript
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>;
|
|
};
|
|
|
|
export class AppError extends Error {
|
|
constructor(
|
|
message: string,
|
|
public code: string,
|
|
public userMessage?: string
|
|
) {
|
|
super(message);
|
|
this.name = 'AppError';
|
|
}
|
|
}
|
|
|
|
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);
|
|
|
|
const errorMessage = error instanceof AppError
|
|
? error.userMessage || error.message
|
|
: error instanceof Error
|
|
? error.message
|
|
: 'An unexpected error occurred';
|
|
|
|
// Log to console/monitoring with enhanced debugging
|
|
const stack = error instanceof Error ? error.stack : undefined;
|
|
|
|
logger.error('Error occurred', {
|
|
...context,
|
|
error: error instanceof Error ? error.message : String(error),
|
|
stack,
|
|
errorId,
|
|
errorType: typeof error,
|
|
errorConstructor: error?.constructor?.name,
|
|
hasStack: !!stack,
|
|
});
|
|
|
|
// 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: 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),
|
|
p_timezone: envContext.timezone,
|
|
p_referrer: document.referrer || undefined,
|
|
}).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
|
|
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),
|
|
p_timezone: envContext.timezone,
|
|
p_referrer: document.referrer || undefined,
|
|
}).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'
|
|
);
|
|
}
|