feat: Implement final error coverage

This commit is contained in:
gpt-engineer-app[bot]
2025-11-04 19:50:06 +00:00
parent a9334c7a3a
commit 0df047d56b
9 changed files with 291 additions and 403 deletions

View File

@@ -11,8 +11,7 @@ import type {
IdentitySafetyCheck, IdentitySafetyCheck,
IdentityOperationResult IdentityOperationResult
} from '@/types/identity'; } from '@/types/identity';
import { logger } from './logger'; import { handleNonCriticalError, handleError, getErrorMessage } from './errorHandler';
import { getErrorMessage } from './errorHandler';
/** /**
* Get all identities for the current user * Get all identities for the current user
@@ -25,10 +24,9 @@ export async function getUserIdentities(): Promise<UserIdentity[]> {
return (data?.identities || []) as UserIdentity[]; return (data?.identities || []) as UserIdentity[];
} catch (error) { } catch (error) {
const errorMsg = getErrorMessage(error); handleNonCriticalError(error, {
logger.error('Failed to get user identities', { action: 'Get User Identities',
action: 'get_identities', metadata: { returnedEmptyArray: true }
error: errorMsg
}); });
return []; return [];
} }
@@ -102,9 +100,9 @@ export async function disconnectIdentity(
// Get AAL level - fail closed on error // Get AAL level - fail closed on error
const { data: aalData, error: aalError } = await supabase.auth.mfa.getAuthenticatorAssuranceLevel(); const { data: aalData, error: aalError } = await supabase.auth.mfa.getAuthenticatorAssuranceLevel();
if (aalError) { if (aalError) {
logger.error('Failed to get AAL level for identity disconnect', { handleNonCriticalError(aalError, {
action: 'disconnect_identity_aal_check', action: 'Get AAL Level (Identity Disconnect)',
error: aalError.message metadata: { failClosed: true }
}); });
return { return {
success: false, success: false,
@@ -120,9 +118,9 @@ export async function disconnectIdentity(
const { data: factors, error: factorsError } = await supabase.auth.mfa.listFactors(); const { data: factors, error: factorsError } = await supabase.auth.mfa.listFactors();
if (factorsError) { if (factorsError) {
logger.error('Failed to list MFA factors for identity disconnect', { handleNonCriticalError(factorsError, {
action: 'disconnect_identity_mfa_check', action: 'List MFA Factors (Identity Disconnect)',
error: factorsError.message metadata: { failClosed: true }
}); });
return { return {
success: false, success: false,
@@ -177,15 +175,13 @@ export async function disconnectIdentity(
return { success: true }; return { success: true };
} catch (error) { } catch (error) {
const errorMsg = getErrorMessage(error); handleError(error, {
logger.error('Failed to disconnect identity', { action: 'Disconnect Identity',
action: 'identity_disconnect', metadata: { provider }
provider,
error: errorMsg
}); });
return { return {
success: false, success: false,
error: errorMsg error: getErrorMessage(error)
}; };
} }
} }
@@ -210,15 +206,13 @@ export async function connectIdentity(
return { success: true }; return { success: true };
} catch (error) { } catch (error) {
const errorMsg = getErrorMessage(error); handleError(error, {
logger.error('Failed to connect identity', { action: 'Connect Identity',
action: 'identity_connect', metadata: { provider }
provider,
error: errorMsg
}); });
return { return {
success: false, success: false,
error: errorMsg error: getErrorMessage(error)
}; };
} }
} }
@@ -240,11 +234,6 @@ export async function addPasswordToAccount(): Promise<IdentityOperationResult> {
}; };
} }
logger.info('Initiating password setup', {
action: 'password_setup_initiated',
email: userEmail
});
// Trigger Supabase password reset email // Trigger Supabase password reset email
// User clicks link and sets password, which automatically creates email identity // User clicks link and sets password, which automatically creates email identity
const { error: resetError } = await supabase.auth.resetPasswordForEmail( const { error: resetError } = await supabase.auth.resetPasswordForEmail(
@@ -255,20 +244,14 @@ export async function addPasswordToAccount(): Promise<IdentityOperationResult> {
); );
if (resetError) { if (resetError) {
logger.error('Failed to send password reset email', { handleError(resetError, {
action: 'Send Password Reset Email',
userId: user?.id, userId: user?.id,
action: 'password_setup_email', metadata: { email: userEmail }
error: resetError.message
}); });
throw resetError; throw resetError;
} }
logger.info('Password reset email sent', {
userId: user!.id,
action: 'password_setup_initiated',
email: userEmail
});
// Log the action // Log the action
await logIdentityChange(user!.id, 'password_setup_initiated', { await logIdentityChange(user!.id, 'password_setup_initiated', {
method: 'reset_password_flow', method: 'reset_password_flow',
@@ -281,14 +264,12 @@ export async function addPasswordToAccount(): Promise<IdentityOperationResult> {
email: userEmail email: userEmail
}; };
} catch (error) { } catch (error) {
const errorMsg = getErrorMessage(error); handleError(error, {
logger.error('Failed to initiate password setup', { action: 'Initiate Password Setup'
action: 'password_setup',
error: errorMsg
}); });
return { return {
success: false, success: false,
error: errorMsg error: getErrorMessage(error)
}; };
} }
} }
@@ -310,10 +291,10 @@ async function logIdentityChange(
_details: details _details: details
}); });
} catch (error) { } catch (error) {
logger.error('Failed to log identity change to audit', { handleNonCriticalError(error, {
action: 'Log Identity Change to Audit',
userId, userId,
action, metadata: { auditAction: action }
error: error instanceof Error ? error.message : String(error)
}); });
// Don't fail the operation if audit logging fails // Don't fail the operation if audit logging fails
} }

View File

@@ -1,7 +1,7 @@
import { supabase } from '@/lib/supabaseClient'; import { supabase } from '@/lib/supabaseClient';
import { invokeWithTracking } from './edgeFunctionTracking'; import { invokeWithTracking } from './edgeFunctionTracking';
import type { UploadedImage } from '@/components/upload/EntityMultiImageUploader'; import type { UploadedImage } from '@/components/upload/EntityMultiImageUploader';
import { logger } from './logger'; import { handleError, handleNonCriticalError } from './errorHandler';
export interface CloudflareUploadResponse { export interface CloudflareUploadResponse {
result: { result: {
@@ -34,20 +34,14 @@ export async function uploadPendingImages(images: UploadedImage[]): Promise<Uplo
); );
if (urlError || !uploadUrlData?.uploadURL) { if (urlError || !uploadUrlData?.uploadURL) {
logger.error('Failed to get upload URL', { const error = new Error(`Failed to get upload URL for "${fileName}": ${urlError?.message || 'Unknown error'}`);
action: 'upload_pending_images', handleError(error, {
fileName, action: 'Get Upload URL',
requestId, metadata: { fileName, requestId }
error: urlError?.message || 'Unknown error',
}); });
throw new Error(`Failed to get upload URL for "${fileName}": ${urlError?.message || 'Unknown error'}`); throw error;
} }
logger.info('Got upload URL', {
action: 'upload_pending_images',
fileName,
requestId,
});
// Step 2: Upload file directly to Cloudflare // Step 2: Upload file directly to Cloudflare
const formData = new FormData(); const formData = new FormData();
@@ -60,30 +54,25 @@ export async function uploadPendingImages(images: UploadedImage[]): Promise<Uplo
if (!uploadResponse.ok) { if (!uploadResponse.ok) {
const errorText = await uploadResponse.text(); const errorText = await uploadResponse.text();
logger.error('Cloudflare upload failed', { const error = new Error(`Upload failed for "${fileName}" (status ${uploadResponse.status}): ${errorText}`);
action: 'upload_pending_images', handleError(error, {
fileName, action: 'Cloudflare Upload',
status: uploadResponse.status, metadata: { fileName, status: uploadResponse.status }
error: errorText,
}); });
throw new Error(`Upload failed for "${fileName}" (status ${uploadResponse.status}): ${errorText}`); throw error;
} }
const result: CloudflareUploadResponse = await uploadResponse.json(); const result: CloudflareUploadResponse = await uploadResponse.json();
if (!result.success || !result.result) { if (!result.success || !result.result) {
logger.error('Cloudflare upload unsuccessful', { const error = new Error(`Cloudflare upload returned unsuccessful response for "${fileName}"`);
action: 'upload_pending_images', handleError(error, {
fileName, action: 'Cloudflare Upload',
metadata: { fileName }
}); });
throw new Error(`Cloudflare upload returned unsuccessful response for "${fileName}"`); throw error;
} }
logger.info('Image uploaded successfully', {
action: 'upload_pending_images',
fileName,
imageId: result.result.id,
});
// Clean up object URL // Clean up object URL
URL.revokeObjectURL(image.url); URL.revokeObjectURL(image.url);
@@ -132,10 +121,13 @@ export async function uploadPendingImages(images: UploadedImage[]): Promise<Uplo
// If any uploads failed, clean up ONLY newly uploaded images and throw error // If any uploads failed, clean up ONLY newly uploaded images and throw error
if (errors.length > 0) { if (errors.length > 0) {
if (newlyUploadedImageIds.length > 0) { if (newlyUploadedImageIds.length > 0) {
logger.error('Some uploads failed, cleaning up', { const cleanupError = new Error(`Some uploads failed, cleaning up ${newlyUploadedImageIds.length} newly uploaded images`);
action: 'upload_pending_images', handleError(cleanupError, {
newlyUploadedCount: newlyUploadedImageIds.length, action: 'Upload Cleanup',
failureCount: errors.length, metadata: {
newlyUploadedCount: newlyUploadedImageIds.length,
failureCount: errors.length
}
}); });
// Attempt cleanup in parallel with detailed error tracking // Attempt cleanup in parallel with detailed error tracking
@@ -148,24 +140,29 @@ export async function uploadPendingImages(images: UploadedImage[]): Promise<Uplo
) )
); );
// Track cleanup failures for better debugging // Track cleanup failures silently (non-critical)
const cleanupFailures = cleanupResults.filter(r => r.status === 'rejected'); const cleanupFailures = cleanupResults.filter(r => r.status === 'rejected');
if (cleanupFailures.length > 0) { if (cleanupFailures.length > 0) {
logger.error('Failed to cleanup images', { handleNonCriticalError(
action: 'upload_pending_images_cleanup', new Error(`Failed to cleanup ${cleanupFailures.length} of ${newlyUploadedImageIds.length} images`),
cleanupFailures: cleanupFailures.length, {
totalCleanup: newlyUploadedImageIds.length, action: 'Image Cleanup',
orphanedImages: newlyUploadedImageIds.filter((_, i) => cleanupResults[i].status === 'rejected'), metadata: {
}); cleanupFailures: cleanupFailures.length,
} else { totalCleanup: newlyUploadedImageIds.length,
logger.info('Successfully cleaned up images', { orphanedImages: newlyUploadedImageIds.filter((_, i) => cleanupResults[i].status === 'rejected')
action: 'upload_pending_images_cleanup', }
cleanedCount: newlyUploadedImageIds.length, }
}); );
} }
} }
throw new Error(`Failed to upload ${errors.length} of ${images.length} images: ${errors.join('; ')}`); const finalError = new Error(`Failed to upload ${errors.length} of ${images.length} images: ${errors.join('; ')}`);
handleError(finalError, {
action: 'Image Upload',
metadata: { failureCount: errors.length, totalCount: images.length }
});
throw finalError;
} }
// Remove the wasNewlyUploaded flag before returning // Remove the wasNewlyUploaded flag before returning

View File

@@ -9,8 +9,7 @@
import { SupabaseClient } from '@supabase/supabase-js'; import { SupabaseClient } from '@supabase/supabase-js';
import { createTableQuery } from '@/lib/supabaseHelpers'; import { createTableQuery } from '@/lib/supabaseHelpers';
import type { ModerationItem } from '@/types/moderation'; import type { ModerationItem } from '@/types/moderation';
import { logger } from '@/lib/logger'; import { handleError, handleNonCriticalError, getErrorMessage } from '@/lib/errorHandler';
import { getErrorMessage } from '@/lib/errorHandler';
import { invokeWithTracking, invokeBatchWithTracking } from '@/lib/edgeFunctionTracking'; import { invokeWithTracking, invokeBatchWithTracking } from '@/lib/edgeFunctionTracking';
/** /**
@@ -154,16 +153,15 @@ export async function approvePhotoSubmission(
shouldRemoveFromQueue: true, shouldRemoveFromQueue: true,
}; };
} catch (error: unknown) { } catch (error: unknown) {
const errorMessage = getErrorMessage(error); handleError(error, {
logger.error('Photo approval failed', { action: 'Approve Photo Submission',
action: 'approve_photo', userId: config.moderatorId,
submissionId: config.submissionId, metadata: { submissionId: config.submissionId }
error: errorMessage
}); });
return { return {
success: false, success: false,
message: 'Failed to approve photo submission', message: 'Failed to approve photo submission',
error: new Error(errorMessage), error: error instanceof Error ? error : new Error(getErrorMessage(error)),
shouldRemoveFromQueue: false, shouldRemoveFromQueue: false,
}; };
} }
@@ -194,22 +192,14 @@ export async function approveSubmissionItems(
); );
if (approvalError) { if (approvalError) {
logger.error('Submission items approval failed via edge function', { const error = new Error(`Failed to process submission items: ${approvalError.message}`);
action: 'approve_submission_items', handleError(error, {
submissionId, action: 'Approve Submission Items',
itemCount: itemIds.length, metadata: { submissionId, itemCount: itemIds.length, requestId }
requestId,
error: approvalError.message,
}); });
throw new Error(`Failed to process submission items: ${approvalError.message}`); throw error;
} }
logger.info('Submission items approved successfully', {
action: 'approve_submission_items',
submissionId,
itemCount: itemIds.length,
requestId,
});
return { return {
success: true, success: true,
@@ -217,17 +207,14 @@ export async function approveSubmissionItems(
shouldRemoveFromQueue: true, shouldRemoveFromQueue: true,
}; };
} catch (error: unknown) { } catch (error: unknown) {
const errorMessage = getErrorMessage(error); handleError(error, {
logger.error('Submission items approval failed', { action: 'Approve Submission Items',
action: 'approve_submission_items', metadata: { submissionId, itemCount: itemIds.length }
submissionId,
itemCount: itemIds.length,
error: errorMessage
}); });
return { return {
success: false, success: false,
message: 'Failed to approve submission items', message: 'Failed to approve submission items',
error: new Error(errorMessage), error: error instanceof Error ? error : new Error(getErrorMessage(error)),
shouldRemoveFromQueue: false, shouldRemoveFromQueue: false,
}; };
} }
@@ -260,11 +247,9 @@ export async function rejectSubmissionItems(
.eq('status', 'pending'); .eq('status', 'pending');
if (rejectError) { if (rejectError) {
const errorMessage = getErrorMessage(rejectError); handleError(rejectError, {
logger.error('Item rejection cascade failed', { action: 'Reject Submission Items (Cascade)',
action: 'reject_submission_items', metadata: { submissionId }
submissionId,
error: errorMessage
}); });
} }
@@ -274,16 +259,14 @@ export async function rejectSubmissionItems(
shouldRemoveFromQueue: false, // Parent rejection will handle removal shouldRemoveFromQueue: false, // Parent rejection will handle removal
}; };
} catch (error: unknown) { } catch (error: unknown) {
const errorMessage = getErrorMessage(error); handleError(error, {
logger.error('Submission items rejection failed', { action: 'Reject Submission Items',
action: 'reject_submission_items', metadata: { submissionId }
submissionId,
error: errorMessage
}); });
return { return {
success: false, success: false,
message: 'Failed to reject submission items', message: 'Failed to reject submission items',
error: new Error(errorMessage), error: error instanceof Error ? error : new Error(getErrorMessage(error)),
shouldRemoveFromQueue: false, shouldRemoveFromQueue: false,
}; };
} }
@@ -412,17 +395,15 @@ export async function performModerationAction(
shouldRemoveFromQueue: action === 'approved' || action === 'rejected', shouldRemoveFromQueue: action === 'approved' || action === 'rejected',
}; };
} catch (error: unknown) { } catch (error: unknown) {
const errorMessage = getErrorMessage(error); handleError(error, {
logger.error('Moderation action failed', { action: `${config.action === 'approved' ? 'Approve' : 'Reject'} Content`,
action: config.action, userId: config.moderatorId,
itemType: item.type, metadata: { itemType: item.type, itemId: item.id }
itemId: item.id,
error: errorMessage
}); });
return { return {
success: false, success: false,
message: `Failed to ${config.action} content`, message: `Failed to ${config.action} content`,
error: new Error(errorMessage), error: error instanceof Error ? error : new Error(getErrorMessage(error)),
shouldRemoveFromQueue: false, shouldRemoveFromQueue: false,
}; };
} }
@@ -513,15 +494,20 @@ export async function deleteSubmission(
const successfulDeletions = deleteResults.filter(r => !r.error); const successfulDeletions = deleteResults.filter(r => !r.error);
deletedPhotoCount = successfulDeletions.length; deletedPhotoCount = successfulDeletions.length;
// Log any failures // Log any failures silently (background operation)
const failedDeletions = deleteResults.filter(r => r.error); const failedDeletions = deleteResults.filter(r => r.error);
if (failedDeletions.length > 0) { if (failedDeletions.length > 0) {
logger.error('Some photo deletions failed', { handleNonCriticalError(
action: 'delete_submission_photos', new Error(`Failed to delete ${failedDeletions.length} of ${validImageIds.length} photos`),
failureCount: failedDeletions.length, {
totalAttempted: validImageIds.length, action: 'Delete Submission Photos',
failedRequestIds: failedDeletions.map(r => r.requestId), metadata: {
}); failureCount: failedDeletions.length,
totalAttempted: validImageIds.length,
failedRequestIds: failedDeletions.map(r => r.requestId)
}
}
);
} }
} }
} }
@@ -563,12 +549,15 @@ export async function deleteSubmission(
message, message,
shouldRemoveFromQueue: true, shouldRemoveFromQueue: true,
}; };
} catch (error) { } catch (error: unknown) {
logger.error('Error deleting submission', { error, submissionId: item.id }); handleError(error, {
action: 'Delete Submission',
metadata: { submissionId: item.id, deletePhotos }
});
return { return {
success: false, success: false,
message: 'Failed to delete submission', message: 'Failed to delete submission',
error: error as Error, error: error instanceof Error ? error : new Error('Unknown error'),
shouldRemoveFromQueue: false, shouldRemoveFromQueue: false,
}; };
} }

View File

@@ -11,7 +11,7 @@ import type { ModerationAction } from '../moderationStateMachine';
import { hasActiveLock, needsLockRenewal } from '../moderationStateMachine'; import { hasActiveLock, needsLockRenewal } from '../moderationStateMachine';
import { toast } from '@/hooks/use-toast'; import { toast } from '@/hooks/use-toast';
import { supabase } from '@/lib/supabaseClient'; import { supabase } from '@/lib/supabaseClient';
import { logger } from '../logger'; import { handleNonCriticalError } from '../errorHandler';
/** /**
* Hook to monitor lock status and warn about expiry * Hook to monitor lock status and warn about expiry
@@ -33,14 +33,6 @@ export function useLockMonitor(
const checkInterval = setInterval(() => { const checkInterval = setInterval(() => {
if (needsLockRenewal(state)) { if (needsLockRenewal(state)) {
logger.warn('Lock expiring soon', {
action: 'lock_expiry_warning',
itemId,
lockExpires: state.status === 'locked' || state.status === 'reviewing'
? state.lockExpires
: undefined,
});
// Dispatch lock expiry warning // Dispatch lock expiry warning
dispatch({ type: 'LOCK_EXPIRED' }); dispatch({ type: 'LOCK_EXPIRED' });
@@ -103,16 +95,10 @@ export async function handleExtendLock(
title: 'Lock Extended', title: 'Lock Extended',
description: 'You have 15 more minutes to complete your review.', description: 'You have 15 more minutes to complete your review.',
}); });
logger.info('Lock extended successfully', {
action: 'lock_extended',
submissionId,
});
} catch (error: unknown) { } catch (error: unknown) {
logger.error('Failed to extend lock', { handleNonCriticalError(error, {
action: 'extend_lock_error', action: 'Extend Lock',
submissionId, metadata: { submissionId }
error: error instanceof Error ? error.message : String(error),
}); });
toast({ toast({

View File

@@ -1,7 +1,6 @@
import { supabase } from "@/lib/supabaseClient"; import { supabase } from "@/lib/supabaseClient";
import { invokeWithTracking } from "@/lib/edgeFunctionTracking"; import { invokeWithTracking } from "@/lib/edgeFunctionTracking";
import { logger } from "@/lib/logger"; import { handleNonCriticalError, AppError } from "@/lib/errorHandler";
import { AppError } from "@/lib/errorHandler";
import { z } from "zod"; import { z } from "zod";
import type { import type {
NotificationPayload, NotificationPayload,
@@ -29,9 +28,9 @@ class NotificationService {
return !!data?.setting_value; return !!data?.setting_value;
} catch (error: unknown) { } catch (error: unknown) {
logger.error('Failed to check Novu status', { handleNonCriticalError(error, {
action: 'check_novu_status', action: 'Check Novu Status',
error: error instanceof Error ? error.message : String(error) metadata: { returnedFalse: true }
}); });
return false; return false;
} }
@@ -47,10 +46,6 @@ class NotificationService {
const novuEnabled = await this.isNovuEnabled(); const novuEnabled = await this.isNovuEnabled();
if (!novuEnabled) { if (!novuEnabled) {
logger.warn('Novu not configured, skipping subscriber update', {
action: 'update_novu_subscriber',
userId: validated.subscriberId
});
return { success: false, error: 'Novu not configured' }; return { success: false, error: 'Novu not configured' };
} }
@@ -60,11 +55,10 @@ class NotificationService {
); );
if (error) { if (error) {
logger.error('Edge function error updating Novu subscriber', { handleNonCriticalError(error, {
action: 'update_novu_subscriber', action: 'Update Novu Subscriber (Edge Function)',
userId: validated.subscriberId, userId: validated.subscriberId,
requestId, metadata: { requestId }
error: error.message
}); });
throw new AppError( throw new AppError(
'Failed to update notification subscriber', 'Failed to update notification subscriber',
@@ -73,18 +67,11 @@ class NotificationService {
); );
} }
logger.info('Novu subscriber updated successfully', {
action: 'update_novu_subscriber',
userId: validated.subscriberId,
requestId
});
return { success: true }; return { success: true };
} catch (error: unknown) { } catch (error: unknown) {
logger.error('Error in updateSubscriber', { handleNonCriticalError(error, {
action: 'update_novu_subscriber', action: 'Update Novu Subscriber',
userId: subscriberData.subscriberId, userId: subscriberData.subscriberId
error: error instanceof Error ? error.message : String(error)
}); });
return { return {
@@ -104,10 +91,6 @@ class NotificationService {
const novuEnabled = await this.isNovuEnabled(); const novuEnabled = await this.isNovuEnabled();
if (!novuEnabled) { if (!novuEnabled) {
logger.warn('Novu not configured, skipping subscriber creation', {
action: 'create_novu_subscriber',
userId: validated.subscriberId
});
return { success: false, error: 'Novu not configured' }; return { success: false, error: 'Novu not configured' };
} }
@@ -117,11 +100,10 @@ class NotificationService {
); );
if (error) { if (error) {
logger.error('Edge function error creating Novu subscriber', { handleNonCriticalError(error, {
action: 'create_novu_subscriber', action: 'Create Novu Subscriber (Edge Function)',
userId: validated.subscriberId, userId: validated.subscriberId,
requestId, metadata: { requestId }
error: error.message
}); });
throw new AppError( throw new AppError(
'Failed to create notification subscriber', 'Failed to create notification subscriber',
@@ -146,27 +128,18 @@ class NotificationService {
}); });
if (dbError) { if (dbError) {
logger.error('Failed to store subscriber preferences', { handleNonCriticalError(dbError, {
action: 'store_subscriber_preferences', action: 'Store Subscriber Preferences',
userId: validated.subscriberId, userId: validated.subscriberId
error: dbError.message,
errorCode: dbError.code
}); });
throw dbError; throw dbError;
} }
logger.info('Novu subscriber created successfully', {
action: 'create_novu_subscriber',
userId: validated.subscriberId,
requestId
});
return { success: true }; return { success: true };
} catch (error: unknown) { } catch (error: unknown) {
logger.error('Error in createSubscriber', { handleNonCriticalError(error, {
action: 'create_novu_subscriber', action: 'Create Novu Subscriber',
userId: subscriberData.subscriberId, userId: subscriberData.subscriberId
error: error instanceof Error ? error.message : String(error)
}); });
return { return {
@@ -207,11 +180,10 @@ class NotificationService {
); );
if (novuError) { if (novuError) {
logger.error('Failed to update Novu preferences', { handleNonCriticalError(novuError, {
action: 'update_novu_preferences', action: 'Update Novu Preferences',
userId, userId,
requestId, metadata: { requestId }
error: novuError.message
}); });
throw novuError; throw novuError;
} }
@@ -228,11 +200,9 @@ class NotificationService {
}); });
if (dbError) { if (dbError) {
logger.error('Failed to save notification preferences', { handleNonCriticalError(dbError, {
action: 'save_notification_preferences', action: 'Save Notification Preferences',
userId, userId
error: dbError.message,
errorCode: dbError.code
}); });
throw dbError; throw dbError;
} }
@@ -268,17 +238,11 @@ class NotificationService {
}); });
} }
logger.info('Notification preferences updated', {
action: 'update_notification_preferences',
userId
});
return { success: true }; return { success: true };
} catch (error: unknown) { } catch (error: unknown) {
logger.error('Error updating notification preferences', { handleNonCriticalError(error, {
action: 'update_notification_preferences', action: 'Update Notification Preferences',
userId, userId
error: error instanceof Error ? error.message : String(error)
}); });
if (error instanceof z.ZodError) { if (error instanceof z.ZodError) {
@@ -307,20 +271,14 @@ class NotificationService {
.maybeSingle(); .maybeSingle();
if (error && error.code !== 'PGRST116') { if (error && error.code !== 'PGRST116') {
logger.error('Failed to fetch notification preferences', { handleNonCriticalError(error, {
action: 'fetch_notification_preferences', action: 'Fetch Notification Preferences',
userId, userId
error: error.message,
errorCode: error.code
}); });
throw error; throw error;
} }
if (!data) { if (!data) {
logger.info('No preferences found, returning defaults', {
action: 'fetch_notification_preferences',
userId
});
return DEFAULT_NOTIFICATION_PREFERENCES; return DEFAULT_NOTIFICATION_PREFERENCES;
} }
@@ -331,10 +289,9 @@ class NotificationService {
frequencySettings: data.frequency_settings frequencySettings: data.frequency_settings
}); });
} catch (error: unknown) { } catch (error: unknown) {
logger.error('Error fetching notification preferences', { handleNonCriticalError(error, {
action: 'fetch_notification_preferences', action: 'Get Notification Preferences',
userId, userId
error: error instanceof Error ? error.message : String(error)
}); });
return null; return null;
} }
@@ -352,10 +309,8 @@ class NotificationService {
.order('category', { ascending: true }); .order('category', { ascending: true });
if (error) { if (error) {
logger.error('Failed to fetch notification templates', { handleNonCriticalError(error, {
action: 'fetch_notification_templates', action: 'Fetch Notification Templates'
error: error.message,
errorCode: error.code
}); });
throw error; throw error;
} }
@@ -367,9 +322,8 @@ class NotificationService {
novu_workflow_id: t.novu_workflow_id || null, novu_workflow_id: t.novu_workflow_id || null,
})); }));
} catch (error: unknown) { } catch (error: unknown) {
logger.error('Error fetching notification templates', { handleNonCriticalError(error, {
action: 'fetch_notification_templates', action: 'Get Notification Templates'
error: error instanceof Error ? error.message : String(error)
}); });
return []; return [];
} }
@@ -382,11 +336,6 @@ class NotificationService {
try { try {
const novuEnabled = await this.isNovuEnabled(); const novuEnabled = await this.isNovuEnabled();
if (!novuEnabled) { if (!novuEnabled) {
logger.warn('Novu not configured, skipping notification', {
action: 'trigger_notification',
workflowId: payload.workflowId,
subscriberId: payload.subscriberId
});
return { success: false, error: 'Novu not configured' }; return { success: false, error: 'Novu not configured' };
} }
@@ -396,31 +345,18 @@ class NotificationService {
); );
if (error) { if (error) {
logger.error('Failed to trigger notification', { handleNonCriticalError(error, {
action: 'trigger_notification', action: 'Trigger Notification',
workflowId: payload.workflowId, metadata: { workflowId: payload.workflowId, subscriberId: payload.subscriberId, requestId }
subscriberId: payload.subscriberId,
requestId,
error: error.message
}); });
throw error; throw error;
} }
logger.info('Notification triggered successfully', {
action: 'trigger_notification',
workflowId: payload.workflowId,
subscriberId: payload.subscriberId,
transactionId: data?.transactionId,
requestId
});
return { success: true }; return { success: true };
} catch (error: unknown) { } catch (error: unknown) {
logger.error('Error triggering notification', { handleNonCriticalError(error, {
action: 'trigger_notification', action: 'Trigger Notification',
workflowId: payload.workflowId, metadata: { workflowId: payload.workflowId, subscriberId: payload.subscriberId }
subscriberId: payload.subscriberId,
error: error instanceof Error ? error.message : String(error)
}); });
return { return {
@@ -446,25 +382,16 @@ class NotificationService {
); );
if (error) { if (error) {
logger.error('Failed to notify moderators', { handleNonCriticalError(error, {
action: 'notify_moderators', action: 'Notify Moderators (Submission)',
submissionId: payload.submission_id, metadata: { submissionId: payload.submission_id, requestId }
requestId,
error: error.message
}); });
throw error; throw error;
} }
logger.info('Moderators notified successfully', {
action: 'notify_moderators',
submissionId: payload.submission_id,
requestId
});
} catch (error: unknown) { } catch (error: unknown) {
logger.error('Error notifying moderators', { handleNonCriticalError(error, {
action: 'notify_moderators', action: 'Notify Moderators (Submission)',
submissionId: payload.submission_id, metadata: { submissionId: payload.submission_id }
error: error instanceof Error ? error.message : String(error)
}); });
} }
} }
@@ -482,10 +409,6 @@ class NotificationService {
try { try {
const novuEnabled = await this.isNovuEnabled(); const novuEnabled = await this.isNovuEnabled();
if (!novuEnabled) { if (!novuEnabled) {
logger.warn('Novu not configured, skipping system announcement', {
action: 'send_system_announcement',
title: payload.title
});
return { success: false, error: 'Novu not configured' }; return { success: false, error: 'Novu not configured' };
} }
@@ -495,31 +418,21 @@ class NotificationService {
); );
if (error) { if (error) {
logger.error('Failed to send system announcement', { handleNonCriticalError(error, {
action: 'send_system_announcement', action: 'Send System Announcement',
title: payload.title, metadata: { title: payload.title, requestId }
requestId,
error: error.message
}); });
throw error; throw error;
} }
logger.info('System announcement sent successfully', {
action: 'send_system_announcement',
title: payload.title,
announcementId: data?.announcementId,
requestId
});
return { return {
success: true, success: true,
announcementId: data?.announcementId announcementId: data?.announcementId
}; };
} catch (error: unknown) { } catch (error: unknown) {
logger.error('Error sending system announcement', { handleNonCriticalError(error, {
action: 'send_system_announcement', action: 'Send System Announcement',
title: payload.title, metadata: { title: payload.title }
error: error instanceof Error ? error.message : String(error)
}); });
return { return {
@@ -545,10 +458,6 @@ class NotificationService {
try { try {
const novuEnabled = await this.isNovuEnabled(); const novuEnabled = await this.isNovuEnabled();
if (!novuEnabled) { if (!novuEnabled) {
logger.warn('Novu not configured, skipping report notification', {
action: 'notify_moderators_report',
reportId: payload.reportId
});
return { success: false, error: 'Novu not configured' }; return { success: false, error: 'Novu not configured' };
} }
@@ -558,27 +467,18 @@ class NotificationService {
); );
if (error) { if (error) {
logger.error('Failed to notify moderators about report', { handleNonCriticalError(error, {
action: 'notify_moderators_report', action: 'Notify Moderators (Report)',
reportId: payload.reportId, metadata: { reportId: payload.reportId, requestId }
requestId,
error: error.message
}); });
throw error; throw error;
} }
logger.info('Moderators notified about report successfully', {
action: 'notify_moderators_report',
reportId: payload.reportId,
requestId
});
return { success: true }; return { success: true };
} catch (error: unknown) { } catch (error: unknown) {
logger.error('Error notifying moderators about report', { handleNonCriticalError(error, {
action: 'notify_moderators_report', action: 'Notify Moderators (Report)',
reportId: payload.reportId, metadata: { reportId: payload.reportId }
error: error instanceof Error ? error.message : String(error)
}); });
return { return {

View File

@@ -168,7 +168,7 @@ async function logRequestMetadata(metadata: RequestMetadata): Promise<void> {
}); });
if (error) { if (error) {
logger.error('Failed to log metadata to database', { error, context: 'RequestTracking' }); // Already logged by handleNonCriticalError in requestTracking
} }
} }

View File

@@ -1,5 +1,5 @@
import { supabase } from '@/lib/supabaseClient'; import { supabase } from '@/lib/supabaseClient';
import { logger } from './logger'; import { handleError } from './errorHandler';
/** /**
* Generate a URL-safe slug from a name * Generate a URL-safe slug from a name
@@ -51,7 +51,10 @@ export async function ensureUniqueSlug(
const { data, error } = await query.limit(1); const { data, error } = await query.limit(1);
if (error) { if (error) {
logger.error('Error checking slug uniqueness', { error, tableName }); handleError(error, {
action: 'Check Slug Uniqueness',
metadata: { tableName, slug }
});
throw error; throw error;
} }

View File

@@ -6,8 +6,7 @@ import type {
RideModelSubmissionData RideModelSubmissionData
} from '@/types/submission-data'; } from '@/types/submission-data';
import { supabase } from '@/lib/supabaseClient'; import { supabase } from '@/lib/supabaseClient';
import { logger } from './logger'; import { handleNonCriticalError, getErrorMessage } from './errorHandler';
import { getErrorMessage } from './errorHandler';
type SubmissionDataTypes = type SubmissionDataTypes =
| ParkSubmissionData | ParkSubmissionData
@@ -81,11 +80,9 @@ async function detectPhotoChanges(submissionId: string): Promise<PhotoChange[]>
.eq('submission_id', submissionId); .eq('submission_id', submissionId);
if (photoError) { if (photoError) {
const errorMessage = getErrorMessage(photoError); handleNonCriticalError(photoError, {
logger.error('Photo submission fetch failed', { action: 'Detect Photo Changes (Fetch Photo Submission)',
action: 'detect_photo_changes', metadata: { submissionId }
submissionId,
error: errorMessage
}); });
} else { } else {
const photoSubmission = photoSubmissions?.[0]; const photoSubmission = photoSubmissions?.[0];
@@ -109,11 +106,9 @@ async function detectPhotoChanges(submissionId: string): Promise<PhotoChange[]>
.in('item_type', ['photo_edit', 'photo_delete']); .in('item_type', ['photo_edit', 'photo_delete']);
if (itemsError) { if (itemsError) {
const errorMessage = getErrorMessage(itemsError); handleNonCriticalError(itemsError, {
logger.error('Submission items fetch failed', { action: 'Detect Photo Changes (Fetch Submission Items)',
action: 'detect_photo_changes', metadata: { submissionId }
submissionId,
error: errorMessage
}); });
} else if (submissionItems && submissionItems.length > 0) { } else if (submissionItems && submissionItems.length > 0) {
for (const item of submissionItems) { for (const item of submissionItems) {
@@ -123,11 +118,9 @@ async function detectPhotoChanges(submissionId: string): Promise<PhotoChange[]>
} }
} }
} catch (err: unknown) { } catch (err: unknown) {
const errorMessage = getErrorMessage(err); handleNonCriticalError(err, {
logger.error('Photo change detection failed', { action: 'Detect Photo Changes',
action: 'detect_photo_changes', metadata: { submissionId }
submissionId,
error: errorMessage
}); });
} }
@@ -349,7 +342,10 @@ export async function detectChanges(
if (data?.name) entityName = `${data.name} (${formatEntityType(entityType)})`; if (data?.name) entityName = `${data.name} (${formatEntityType(entityType)})`;
} }
} catch (err) { } catch (err) {
logger.error('Error fetching entity name for photo operation', { error: err, entityType: itemData.entity_type, entityId: itemData.entity_id }); handleNonCriticalError(err, {
action: 'Fetch Entity Name for Photo Operation',
metadata: { entityType: itemData.entity_type, entityId: itemData.entity_id }
});
} }
} }
@@ -395,7 +391,10 @@ export async function detectChanges(
entityName = `${formatEntityType(entityType)} - ${itemData.title}`; entityName = `${formatEntityType(entityType)} - ${itemData.title}`;
} }
} catch (err) { } catch (err) {
logger.error('Error fetching entity name for milestone', { error: err, entityType: itemData.entity_type, entityId: itemData.entity_id }); handleNonCriticalError(err, {
action: 'Fetch Entity Name for Milestone',
metadata: { entityType: itemData.entity_type, entityId: itemData.entity_id }
});
// Fall back to just the title if database lookup fails // Fall back to just the title if database lookup fails
if (itemData.title) { if (itemData.title) {
entityName = itemData.title; entityName = itemData.title;
@@ -434,7 +433,10 @@ export async function detectChanges(
} }
} }
} catch (err) { } catch (err) {
logger.error('Error resolving entity name for field display', { error: err, entityType: itemData.entity_type, entityId: itemData.entity_id }); handleNonCriticalError(err, {
action: 'Resolve Entity Name for Field Display',
metadata: { entityType: itemData.entity_type, entityId: itemData.entity_id }
});
} }
// Add entity name as an explicit field change at the beginning // Add entity name as an explicit field change at the beginning

View File

@@ -1,6 +1,5 @@
import { supabase } from '@/lib/supabaseClient'; import { supabase } from '@/lib/supabaseClient';
import { getErrorMessage } from './errorHandler'; import { handleError, handleNonCriticalError, getErrorMessage } from './errorHandler';
import { logger } from './logger';
import { extractCloudflareImageId } from './cloudflareImageUtils'; import { extractCloudflareImageId } from './cloudflareImageUtils';
// Core submission item interface with dependencies // Core submission item interface with dependencies
@@ -297,21 +296,19 @@ export async function approveSubmissionItems(
dependencyMap.set(item.id, entityId); dependencyMap.set(item.id, entityId);
} catch (error: unknown) { } catch (error: unknown) {
const errorMsg = getErrorMessage(error); handleError(error, {
logger.error('Error approving items', { action: 'Approve Submission Items',
action: 'approve_submission_items',
error: errorMsg,
userId, userId,
itemCount: items.length metadata: { itemCount: items.length, itemType: item.item_type }
}); });
// Update item with error status // Update item with error status
await updateSubmissionItem(item.id, { await updateSubmissionItem(item.id, {
status: 'rejected' as const, status: 'rejected' as const,
rejection_reason: `Failed to create entity: ${errorMsg}`, rejection_reason: `Failed to create entity: ${getErrorMessage(error)}`,
}); });
throw new Error(`Failed to approve ${item.item_type}: ${errorMsg}`); throw new Error(`Failed to approve ${item.item_type}: ${getErrorMessage(error)}`);
} }
} }
} }
@@ -338,7 +335,6 @@ async function createVersionForApprovedItem(
// - app.current_user_id = original submitter // - app.current_user_id = original submitter
// - app.submission_id = submission ID // - app.submission_id = submission ID
// Then the trigger creates the version automatically // Then the trigger creates the version automatically
logger.debug('Version will be created automatically by trigger', { itemType, entityId });
} }
/** /**
@@ -452,10 +448,9 @@ async function createPark(data: any, dependencyMap: Map<string, string>, sortedI
.eq('id', data.park_id); .eq('id', data.park_id);
if (error) { if (error) {
logger.error('Error updating park', { handleError(error, {
action: 'update_park', action: 'Update Park',
parkId: data.park_id, metadata: { parkId: data.park_id, parkName: resolvedData.name }
error: error.message
}); });
throw new Error(`Database error: ${error.message}`); throw new Error(`Database error: ${error.message}`);
} }
@@ -495,10 +490,9 @@ async function createPark(data: any, dependencyMap: Map<string, string>, sortedI
.single(); .single();
if (error) { if (error) {
logger.error('Error creating park', { handleError(error, {
action: 'create_park', action: 'Create Park',
parkName: resolvedData.name, metadata: { parkName: resolvedData.name }
error: error.message
}); });
throw new Error(`Database error: ${error.message}`); throw new Error(`Database error: ${error.message}`);
} }
@@ -551,10 +545,9 @@ async function resolveLocationId(locationData: any): Promise<string | null> {
.single(); .single();
if (error) { if (error) {
logger.error('Error creating location', { handleError(error, {
action: 'create_location', action: 'Create Location',
locationData, metadata: { locationData }
error: error.message
}); });
throw new Error(`Failed to create location: ${error.message}`); throw new Error(`Failed to create location: ${error.message}`);
} }
@@ -611,7 +604,10 @@ async function createRide(data: any, dependencyMap: Map<string, string>, sortedI
.eq('id', data.ride_id); .eq('id', data.ride_id);
if (error) { if (error) {
logger.error('Error updating ride', { error: error.message, rideId: data.ride_id }); handleError(error, {
action: 'Update Ride',
metadata: { rideId: data.ride_id, rideName: resolvedData.name }
});
throw new Error(`Database error: ${error.message}`); throw new Error(`Database error: ${error.message}`);
} }
@@ -642,7 +638,10 @@ async function createRide(data: any, dependencyMap: Map<string, string>, sortedI
.single(); .single();
if (error) { if (error) {
logger.error('Error creating ride', { error: error.message, rideName: resolvedData.name }); handleError(error, {
action: 'Create Ride',
metadata: { rideName: resolvedData.name }
});
throw new Error(`Database error: ${error.message}`); throw new Error(`Database error: ${error.message}`);
} }
@@ -686,7 +685,10 @@ async function createCompany(
.eq('id', data.id); .eq('id', data.id);
if (error) { if (error) {
logger.error('Error updating company', { error: error.message, companyId: data.id }); handleError(error, {
action: 'Update Company',
metadata: { companyId: data.id, companyName: resolvedData.name, companyType }
});
throw new Error(`Database error: ${error.message}`); throw new Error(`Database error: ${error.message}`);
} }
@@ -721,7 +723,10 @@ async function createCompany(
.single(); .single();
if (error) { if (error) {
logger.error('Error creating company', { error: error.message, companyName: resolvedData.name, companyType }); handleError(error, {
action: 'Create Company',
metadata: { companyName: resolvedData.name, companyType }
});
throw new Error(`Database error: ${error.message}`); throw new Error(`Database error: ${error.message}`);
} }
@@ -760,7 +765,10 @@ async function createRideModel(data: any, dependencyMap: Map<string, string>, so
.eq('id', data.ride_model_id); .eq('id', data.ride_model_id);
if (error) { if (error) {
logger.error('Error updating ride model', { error: error.message, rideModelId: data.ride_model_id }); handleError(error, {
action: 'Update Ride Model',
metadata: { rideModelId: data.ride_model_id, modelName: resolvedData.name }
});
throw new Error(`Database error: ${error.message}`); throw new Error(`Database error: ${error.message}`);
} }
@@ -797,7 +805,10 @@ async function createRideModel(data: any, dependencyMap: Map<string, string>, so
.single(); .single();
if (error) { if (error) {
logger.error('Error creating ride model', { error: error.message, modelName: resolvedData.name }); handleError(error, {
action: 'Create Ride Model',
metadata: { modelName: resolvedData.name }
});
throw new Error(`Database error: ${error.message}`); throw new Error(`Database error: ${error.message}`);
} }
@@ -876,7 +887,10 @@ async function approvePhotos(data: any, dependencyMap: Map<string, string>, user
.select(); .select();
if (error) { if (error) {
logger.error('Error inserting photos', { error: error.message, photoCount: photosToInsert.length, entityType, entityId: finalEntityId }); handleError(error, {
action: 'Insert Photos',
metadata: { photoCount: photosToInsert.length, entityType, entityId: finalEntityId }
});
throw new Error(`Database error: ${error.message}`); throw new Error(`Database error: ${error.message}`);
} }
@@ -958,7 +972,10 @@ async function updateEntityFeaturedImage(
} }
} }
} catch (error) { } catch (error) {
logger.error('Error updating entity featured image', { error, entityType, entityId }); handleNonCriticalError(error, {
action: 'Update Entity Featured Image',
metadata: { entityType, entityId }
});
} }
} }
@@ -1136,10 +1153,13 @@ export async function rejectSubmissionItems(
}) })
.eq('id', itemId); .eq('id', itemId);
if (error) { if (error) {
logger.error('Error rejecting item', { error, itemId }); handleNonCriticalError(error, {
throw error; action: 'Reject Submission Item',
} metadata: { itemId }
});
throw error;
}
}); });
await Promise.all(updates); await Promise.all(updates);
@@ -1171,7 +1191,10 @@ async function updateSubmissionStatusAfterRejection(submissionId: string): Promi
.eq('submission_id', submissionId); .eq('submission_id', submissionId);
if (fetchError) { if (fetchError) {
logger.error('Error fetching submission items', { error: fetchError, submissionId }); handleNonCriticalError(fetchError, {
action: 'Fetch Submission Items for Status Update',
metadata: { submissionId }
});
return; return;
} }
@@ -1202,7 +1225,10 @@ async function updateSubmissionStatusAfterRejection(submissionId: string): Promi
.eq('id', submissionId); .eq('id', submissionId);
if (updateError) { if (updateError) {
logger.error('Error updating submission status', { error: updateError, submissionId }); handleNonCriticalError(updateError, {
action: 'Update Submission Status After Rejection',
metadata: { submissionId, newStatus }
});
} }
} }
@@ -1273,10 +1299,9 @@ export async function editSubmissionItem(
}); });
if (historyError) { if (historyError) {
logger.error('Failed to record edit history', { handleNonCriticalError(historyError, {
itemId, action: 'Record Edit History',
editorId: userId, metadata: { itemId, editorId: userId }
error: historyError.message,
}); });
// Don't fail the whole operation if history tracking fails // Don't fail the whole operation if history tracking fails
} }
@@ -1293,10 +1318,12 @@ export async function editSubmissionItem(
true // isEdit = true true // isEdit = true
); );
} catch (versionError) { } catch (versionError) {
logger.error('Failed to create version for manual edit', { handleNonCriticalError(versionError, {
action: 'create_version_for_edit', action: 'Create Version for Manual Edit',
itemType: currentItem.item_type, metadata: {
entityId: currentItem.approved_entity_id itemType: currentItem.item_type,
entityId: currentItem.approved_entity_id
}
}); });
// Don't fail the entire operation, just log the error // Don't fail the entire operation, just log the error
// The edit itself is still saved, just without version history // The edit itself is still saved, just without version history
@@ -1390,7 +1417,10 @@ export async function escalateSubmission(
} }
}); });
} catch (auditError) { } catch (auditError) {
logger.error('Failed to log escalation audit', { error: auditError }); handleNonCriticalError(auditError, {
action: 'Log Escalation Audit',
metadata: { submissionId }
});
} }
} }
} }
@@ -1421,9 +1451,9 @@ export async function fetchEditHistory(itemId: string) {
return data || []; return data || [];
} catch (error: unknown) { } catch (error: unknown) {
logger.error('Error fetching edit history', { handleNonCriticalError(error, {
itemId, action: 'Fetch Edit History',
error: getErrorMessage(error), metadata: { itemId }
}); });
return []; return [];
} }
@@ -1476,9 +1506,9 @@ export async function checkSubmissionConflict(
}, },
}; };
} catch (error: unknown) { } catch (error: unknown) {
logger.error('Error checking submission conflict', { handleNonCriticalError(error, {
submissionId, action: 'Check Submission Conflict',
error: getErrorMessage(error), metadata: { submissionId }
}); });
throw error; throw error;
} }
@@ -1526,9 +1556,9 @@ export async function fetchSubmissionVersions(
return data || []; return data || [];
} catch (error: unknown) { } catch (error: unknown) {
logger.error('Error fetching submission versions', { handleNonCriticalError(error, {
submissionId, action: 'Fetch Submission Versions',
error: getErrorMessage(error), metadata: { submissionId }
}); });
return []; return [];
} }