From 6e64b80106ad4a8e5b46d957bf4b542f914f8d1d Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Tue, 4 Nov 2025 19:04:06 +0000 Subject: [PATCH] feat: Implement comprehensive error logging --- src/components/auth/AuthModal.tsx | 16 +++-- src/components/settings/EmailChangeDialog.tsx | 27 ++++----- src/hooks/useEntityVersions.ts | 20 ++++++- src/hooks/useLocationAutoDetect.ts | 10 +++- src/hooks/useModerationQueue.ts | 13 +++-- src/lib/errorHandler.ts | 58 +++++++++++++++++++ src/lib/requestTracking.ts | 24 +++++++- src/pages/Auth.tsx | 18 +++--- 8 files changed, 151 insertions(+), 35 deletions(-) diff --git a/src/components/auth/AuthModal.tsx b/src/components/auth/AuthModal.tsx index b36cb20b..952b8efb 100644 --- a/src/components/auth/AuthModal.tsx +++ b/src/components/auth/AuthModal.tsx @@ -8,7 +8,7 @@ import { Separator } from '@/components/ui/separator'; import { Zap, Mail, Lock, User, Eye, EyeOff } from 'lucide-react'; import { supabase } from '@/lib/supabaseClient'; import { useToast } from '@/hooks/use-toast'; -import { handleError } from '@/lib/errorHandler'; +import { handleError, handleNonCriticalError } from '@/lib/errorHandler'; import { TurnstileCaptcha } from './TurnstileCaptcha'; import { notificationService } from '@/lib/notificationService'; import { useCaptchaBypass } from '@/hooks/useCaptchaBypass'; @@ -16,8 +16,6 @@ import { MFAChallenge } from './MFAChallenge'; import { verifyMfaUpgrade } from '@/lib/authService'; import { setAuthMethod } from '@/lib/sessionFlags'; import { validateEmailNotDisposable } from '@/lib/emailValidation'; -import { getErrorMessage } from '@/lib/errorHandler'; -import { logger } from '@/lib/logger'; import type { SignInOptions } from '@/types/supabase-auth'; interface AuthModalProps { @@ -276,15 +274,23 @@ export function AuthModal({ open, onOpenChange, defaultTab = 'signin' }: AuthMod if (error) throw error; if (data.user) { + const userId = data.user.id; notificationService.createSubscriber({ - subscriberId: data.user.id, + subscriberId: userId, email: formData.email, firstName: formData.username, data: { username: formData.username, } }).catch(err => { - logger.error('Failed to register Novu subscriber', { error: getErrorMessage(err) }); + handleNonCriticalError(err, { + action: 'Register Novu subscriber', + userId, + metadata: { + email: formData.email, + context: 'post_signup' + } + }); }); } diff --git a/src/components/settings/EmailChangeDialog.tsx b/src/components/settings/EmailChangeDialog.tsx index 659c6e6d..55723e34 100644 --- a/src/components/settings/EmailChangeDialog.tsx +++ b/src/components/settings/EmailChangeDialog.tsx @@ -20,8 +20,7 @@ import { } from '@/components/ui/form'; import { Input } from '@/components/ui/input'; import { Button } from '@/components/ui/button'; -import { handleError, handleSuccess, AppError, getErrorMessage } from '@/lib/errorHandler'; -import { logger } from '@/lib/logger'; +import { handleError, handleSuccess, handleNonCriticalError, AppError, getErrorMessage } from '@/lib/errorHandler'; import { supabase } from '@/lib/supabaseClient'; import { Loader2, Mail, CheckCircle2, AlertCircle } from 'lucide-react'; import { TurnstileCaptcha } from '@/components/auth/TurnstileCaptcha'; @@ -179,10 +178,14 @@ export function EmailChangeDialog({ open, onOpenChange, currentEmail, userId }: } }).then(({ error }) => { if (error) { - logger.error('Failed to log email change', { + handleNonCriticalError(error, { + action: 'Log email change audit', userId, - action: 'email_change_audit_log', - error: error.message + metadata: { + oldEmail: currentEmail, + newEmail: data.newEmail, + auditType: 'email_change_initiated' + } }); } }); @@ -199,10 +202,13 @@ export function EmailChangeDialog({ open, onOpenChange, currentEmail, userId }: timestamp: new Date().toISOString(), } }).catch(error => { - logger.error('Failed to send security notification', { + handleNonCriticalError(error, { + action: 'Send email change notification', userId, - action: 'email_change_notification', - error: error instanceof Error ? error.message : String(error) + metadata: { + notificationType: 'security-alert', + alertType: 'email_change_initiated' + } }); }); } @@ -215,11 +221,6 @@ export function EmailChangeDialog({ open, onOpenChange, currentEmail, userId }: setStep('success'); } catch (error: unknown) { const errorMsg = getErrorMessage(error); - logger.error('Email change failed', { - userId, - action: 'email_change', - error: errorMsg, - }); const hasMessage = error instanceof Error || (typeof error === 'object' && error !== null && 'message' in error); const hasStatus = typeof error === 'object' && error !== null && 'status' in error; diff --git a/src/hooks/useEntityVersions.ts b/src/hooks/useEntityVersions.ts index 4c70b264..e568167a 100644 --- a/src/hooks/useEntityVersions.ts +++ b/src/hooks/useEntityVersions.ts @@ -1,7 +1,7 @@ import { useState, useEffect, useRef, useCallback } from 'react'; import { supabase } from '@/lib/supabaseClient'; import { toast } from 'sonner'; -import { getErrorMessage } from '@/lib/errorHandler'; +import { getErrorMessage, handleNonCriticalError } from '@/lib/errorHandler'; import type { EntityType, EntityVersion } from '@/types/versioning'; import { logger } from '@/lib/logger'; @@ -211,7 +211,14 @@ export function useEntityVersions(entityType: EntityType, entityId: string) { try { supabase.removeChannel(channelRef.current); } catch (error: unknown) { - logger.error('Failed to cleanup realtime subscription', { entityType, entityId, error: getErrorMessage(error) }); + handleNonCriticalError(error, { + action: 'Cleanup realtime subscription', + metadata: { + entityType, + entityId, + context: 'unmount_cleanup' + } + }); } finally { channelRef.current = null; } @@ -243,7 +250,14 @@ export function useEntityVersions(entityType: EntityType, entityId: string) { return () => { if (channelRef.current) { supabase.removeChannel(channelRef.current).catch((error) => { - logger.error('Failed to cleanup realtime subscription', { entityType, entityId, error: getErrorMessage(error) }); + handleNonCriticalError(error, { + action: 'Cleanup realtime subscription', + metadata: { + entityType, + entityId, + context: 'unmount_cleanup' + } + }); }); channelRef.current = null; } diff --git a/src/hooks/useLocationAutoDetect.ts b/src/hooks/useLocationAutoDetect.ts index 55a901fb..b63e2c90 100644 --- a/src/hooks/useLocationAutoDetect.ts +++ b/src/hooks/useLocationAutoDetect.ts @@ -1,6 +1,7 @@ import { useEffect } from 'react'; import { useAuth } from '@/hooks/useAuth'; import { useUnitPreferences } from '@/hooks/useUnitPreferences'; +import { handleNonCriticalError } from '@/lib/errorHandler'; import { logger } from '@/lib/logger'; import * as storage from '@/lib/localStorage'; @@ -26,7 +27,14 @@ export function useLocationAutoDetect() { autoDetectPreferences().then(() => { storage.setItem('location_detection_attempted', 'true'); }).catch((error) => { - logger.error('Failed to auto-detect location', { error }); + handleNonCriticalError(error, { + action: 'Auto-detect user location', + userId: user?.id, + metadata: { + autoDetectEnabled: preferences.auto_detect, + context: 'initial_load' + } + }); storage.setItem('location_detection_attempted', 'true'); }); } diff --git a/src/hooks/useModerationQueue.ts b/src/hooks/useModerationQueue.ts index bfac6a73..24d24358 100644 --- a/src/hooks/useModerationQueue.ts +++ b/src/hooks/useModerationQueue.ts @@ -2,7 +2,7 @@ import { useState, useEffect, useCallback, useRef } from 'react'; import { supabase } from '@/lib/supabaseClient'; import { useAuth } from './useAuth'; import { useToast } from './use-toast'; -import { getErrorMessage } from '@/lib/errorHandler'; +import { getErrorMessage, handleNonCriticalError } from '@/lib/errorHandler'; import { getSubmissionTypeLabel } from '@/lib/moderation/entities'; import { logger } from '@/lib/logger'; @@ -358,9 +358,14 @@ export const useModerationQueue = (config?: UseModerationQueueConfig) => { if (!response.ok) { const errorData = await response.json().catch((parseError) => { - logger.warn('Failed to parse claim error response', { - error: getErrorMessage(parseError), - status: response.status + handleNonCriticalError(parseError, { + action: 'Parse claim error response', + userId: user.id, + metadata: { + submissionId, + httpStatus: response.status, + context: 'claim_submission_error_parsing' + } }); return { message: 'Failed to claim submission' }; }); diff --git a/src/lib/errorHandler.ts b/src/lib/errorHandler.ts index c40a5e8d..2e98dc63 100644 --- a/src/lib/errorHandler.ts +++ b/src/lib/errorHandler.ts @@ -100,6 +100,64 @@ export const handleInfo = ( }); }; +/** + * 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 diff --git a/src/lib/requestTracking.ts b/src/lib/requestTracking.ts index d3a8114d..401e8783 100644 --- a/src/lib/requestTracking.ts +++ b/src/lib/requestTracking.ts @@ -7,6 +7,7 @@ import { supabase } from '@/integrations/supabase/client'; import { requestContext, type RequestContext } from './requestContext'; import { breadcrumbManager } from './errorBreadcrumbs'; import { captureEnvironmentContext } from './environmentContext'; +import { handleNonCriticalError } from './errorHandler'; import { logger } from './logger'; export interface RequestTrackingOptions { @@ -55,7 +56,16 @@ export async function trackRequest( parentRequestId: options.parentRequestId, traceId: context.traceId, }).catch(err => { - logger.error('Failed to log request metadata', { error: err, context: 'RequestTracking' }); + handleNonCriticalError(err, { + action: 'Log request metadata (success)', + userId: options.userId, + metadata: { + endpoint: options.endpoint, + method: options.method, + statusCode: 200, + requestId: context.requestId + } + }); }); // Cleanup context @@ -96,7 +106,17 @@ export async function trackRequest( timezone: envContext.timezone, referrer: typeof document !== 'undefined' ? document.referrer : undefined, }).catch(err => { - logger.error('Failed to log error metadata', { error: err, context: 'RequestTracking' }); + handleNonCriticalError(err, { + action: 'Log request metadata (error)', + userId: options.userId, + metadata: { + endpoint: options.endpoint, + method: options.method, + statusCode: 500, + requestId: context.requestId, + errorType: errorInfo.type + } + }); }); // Cleanup context diff --git a/src/pages/Auth.tsx b/src/pages/Auth.tsx index 9a620eb4..a096def6 100644 --- a/src/pages/Auth.tsx +++ b/src/pages/Auth.tsx @@ -12,8 +12,7 @@ import { Separator } from '@/components/ui/separator'; import { Zap, Mail, Lock, User, AlertCircle, Eye, EyeOff } from 'lucide-react'; import { supabase } from '@/lib/supabaseClient'; import { useToast } from '@/hooks/use-toast'; -import { getErrorMessage } from '@/lib/errorHandler'; -import { logger } from '@/lib/logger'; +import { getErrorMessage, handleNonCriticalError } from '@/lib/errorHandler'; import { TurnstileCaptcha } from '@/components/auth/TurnstileCaptcha'; import { notificationService } from '@/lib/notificationService'; import { StorageWarning } from '@/components/auth/StorageWarning'; @@ -181,8 +180,6 @@ export default function Auth() { // Reset CAPTCHA widget to force fresh token generation setSignInCaptchaKey(prev => prev + 1); - logger.error('[Auth] Sign in error', { error }); - // Enhanced error messages const errorMsg = getErrorMessage(error); let errorMessage = errorMsg; @@ -294,16 +291,23 @@ export default function Auth() { // Register user with Novu (non-blocking) if (data.user) { + const userId = data.user.id; notificationService.createSubscriber({ - subscriberId: data.user.id, + subscriberId: userId, email: formData.email, firstName: formData.username, // Send username as firstName to Novu data: { username: formData.username, } }).catch(err => { - logger.error('Failed to register Novu subscriber', { error: err }); - // Don't block signup if Novu registration fails + handleNonCriticalError(err, { + action: 'Register Novu subscriber', + userId, + metadata: { + email: formData.email, + context: 'post_signup' + } + }); }); }