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

View File

@@ -1,7 +1,7 @@
import { supabase } from '@/lib/supabaseClient';
import { invokeWithTracking } from './edgeFunctionTracking';
import type { UploadedImage } from '@/components/upload/EntityMultiImageUploader';
import { logger } from './logger';
import { handleError, handleNonCriticalError } from './errorHandler';
export interface CloudflareUploadResponse {
result: {
@@ -34,20 +34,14 @@ export async function uploadPendingImages(images: UploadedImage[]): Promise<Uplo
);
if (urlError || !uploadUrlData?.uploadURL) {
logger.error('Failed to get upload URL', {
action: 'upload_pending_images',
fileName,
requestId,
error: urlError?.message || 'Unknown error',
const error = new Error(`Failed to get upload URL for "${fileName}": ${urlError?.message || 'Unknown error'}`);
handleError(error, {
action: 'Get Upload URL',
metadata: { fileName, requestId }
});
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
const formData = new FormData();
@@ -60,30 +54,25 @@ export async function uploadPendingImages(images: UploadedImage[]): Promise<Uplo
if (!uploadResponse.ok) {
const errorText = await uploadResponse.text();
logger.error('Cloudflare upload failed', {
action: 'upload_pending_images',
fileName,
status: uploadResponse.status,
error: errorText,
const error = new Error(`Upload failed for "${fileName}" (status ${uploadResponse.status}): ${errorText}`);
handleError(error, {
action: 'Cloudflare Upload',
metadata: { fileName, status: uploadResponse.status }
});
throw new Error(`Upload failed for "${fileName}" (status ${uploadResponse.status}): ${errorText}`);
throw error;
}
const result: CloudflareUploadResponse = await uploadResponse.json();
if (!result.success || !result.result) {
logger.error('Cloudflare upload unsuccessful', {
action: 'upload_pending_images',
fileName,
const error = new Error(`Cloudflare upload returned unsuccessful response for "${fileName}"`);
handleError(error, {
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
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 (errors.length > 0) {
if (newlyUploadedImageIds.length > 0) {
logger.error('Some uploads failed, cleaning up', {
action: 'upload_pending_images',
newlyUploadedCount: newlyUploadedImageIds.length,
failureCount: errors.length,
const cleanupError = new Error(`Some uploads failed, cleaning up ${newlyUploadedImageIds.length} newly uploaded images`);
handleError(cleanupError, {
action: 'Upload Cleanup',
metadata: {
newlyUploadedCount: newlyUploadedImageIds.length,
failureCount: errors.length
}
});
// 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');
if (cleanupFailures.length > 0) {
logger.error('Failed to cleanup images', {
action: 'upload_pending_images_cleanup',
cleanupFailures: cleanupFailures.length,
totalCleanup: newlyUploadedImageIds.length,
orphanedImages: newlyUploadedImageIds.filter((_, i) => cleanupResults[i].status === 'rejected'),
});
} else {
logger.info('Successfully cleaned up images', {
action: 'upload_pending_images_cleanup',
cleanedCount: newlyUploadedImageIds.length,
});
handleNonCriticalError(
new Error(`Failed to cleanup ${cleanupFailures.length} of ${newlyUploadedImageIds.length} images`),
{
action: 'Image Cleanup',
metadata: {
cleanupFailures: cleanupFailures.length,
totalCleanup: newlyUploadedImageIds.length,
orphanedImages: newlyUploadedImageIds.filter((_, i) => cleanupResults[i].status === 'rejected')
}
}
);
}
}
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

View File

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

View File

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

View File

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

View File

@@ -168,7 +168,7 @@ async function logRequestMetadata(metadata: RequestMetadata): Promise<void> {
});
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 { logger } from './logger';
import { handleError } from './errorHandler';
/**
* Generate a URL-safe slug from a name
@@ -51,7 +51,10 @@ export async function ensureUniqueSlug(
const { data, error } = await query.limit(1);
if (error) {
logger.error('Error checking slug uniqueness', { error, tableName });
handleError(error, {
action: 'Check Slug Uniqueness',
metadata: { tableName, slug }
});
throw error;
}

View File

@@ -6,8 +6,7 @@ import type {
RideModelSubmissionData
} from '@/types/submission-data';
import { supabase } from '@/lib/supabaseClient';
import { logger } from './logger';
import { getErrorMessage } from './errorHandler';
import { handleNonCriticalError, getErrorMessage } from './errorHandler';
type SubmissionDataTypes =
| ParkSubmissionData
@@ -81,11 +80,9 @@ async function detectPhotoChanges(submissionId: string): Promise<PhotoChange[]>
.eq('submission_id', submissionId);
if (photoError) {
const errorMessage = getErrorMessage(photoError);
logger.error('Photo submission fetch failed', {
action: 'detect_photo_changes',
submissionId,
error: errorMessage
handleNonCriticalError(photoError, {
action: 'Detect Photo Changes (Fetch Photo Submission)',
metadata: { submissionId }
});
} else {
const photoSubmission = photoSubmissions?.[0];
@@ -109,11 +106,9 @@ async function detectPhotoChanges(submissionId: string): Promise<PhotoChange[]>
.in('item_type', ['photo_edit', 'photo_delete']);
if (itemsError) {
const errorMessage = getErrorMessage(itemsError);
logger.error('Submission items fetch failed', {
action: 'detect_photo_changes',
submissionId,
error: errorMessage
handleNonCriticalError(itemsError, {
action: 'Detect Photo Changes (Fetch Submission Items)',
metadata: { submissionId }
});
} else if (submissionItems && submissionItems.length > 0) {
for (const item of submissionItems) {
@@ -123,11 +118,9 @@ async function detectPhotoChanges(submissionId: string): Promise<PhotoChange[]>
}
}
} catch (err: unknown) {
const errorMessage = getErrorMessage(err);
logger.error('Photo change detection failed', {
action: 'detect_photo_changes',
submissionId,
error: errorMessage
handleNonCriticalError(err, {
action: 'Detect Photo Changes',
metadata: { submissionId }
});
}
@@ -349,7 +342,10 @@ export async function detectChanges(
if (data?.name) entityName = `${data.name} (${formatEntityType(entityType)})`;
}
} 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}`;
}
} 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
if (itemData.title) {
entityName = itemData.title;
@@ -434,7 +433,10 @@ export async function detectChanges(
}
}
} 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

View File

@@ -1,6 +1,5 @@
import { supabase } from '@/lib/supabaseClient';
import { getErrorMessage } from './errorHandler';
import { logger } from './logger';
import { handleError, handleNonCriticalError, getErrorMessage } from './errorHandler';
import { extractCloudflareImageId } from './cloudflareImageUtils';
// Core submission item interface with dependencies
@@ -297,21 +296,19 @@ export async function approveSubmissionItems(
dependencyMap.set(item.id, entityId);
} catch (error: unknown) {
const errorMsg = getErrorMessage(error);
logger.error('Error approving items', {
action: 'approve_submission_items',
error: errorMsg,
handleError(error, {
action: 'Approve Submission Items',
userId,
itemCount: items.length
metadata: { itemCount: items.length, itemType: item.item_type }
});
// Update item with error status
await updateSubmissionItem(item.id, {
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.submission_id = submission ID
// 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);
if (error) {
logger.error('Error updating park', {
action: 'update_park',
parkId: data.park_id,
error: error.message
handleError(error, {
action: 'Update Park',
metadata: { parkId: data.park_id, parkName: resolvedData.name }
});
throw new Error(`Database error: ${error.message}`);
}
@@ -495,10 +490,9 @@ async function createPark(data: any, dependencyMap: Map<string, string>, sortedI
.single();
if (error) {
logger.error('Error creating park', {
action: 'create_park',
parkName: resolvedData.name,
error: error.message
handleError(error, {
action: 'Create Park',
metadata: { parkName: resolvedData.name }
});
throw new Error(`Database error: ${error.message}`);
}
@@ -551,10 +545,9 @@ async function resolveLocationId(locationData: any): Promise<string | null> {
.single();
if (error) {
logger.error('Error creating location', {
action: 'create_location',
locationData,
error: error.message
handleError(error, {
action: 'Create Location',
metadata: { locationData }
});
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);
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}`);
}
@@ -642,7 +638,10 @@ async function createRide(data: any, dependencyMap: Map<string, string>, sortedI
.single();
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}`);
}
@@ -686,7 +685,10 @@ async function createCompany(
.eq('id', data.id);
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}`);
}
@@ -721,7 +723,10 @@ async function createCompany(
.single();
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}`);
}
@@ -760,7 +765,10 @@ async function createRideModel(data: any, dependencyMap: Map<string, string>, so
.eq('id', data.ride_model_id);
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}`);
}
@@ -797,7 +805,10 @@ async function createRideModel(data: any, dependencyMap: Map<string, string>, so
.single();
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}`);
}
@@ -876,7 +887,10 @@ async function approvePhotos(data: any, dependencyMap: Map<string, string>, user
.select();
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}`);
}
@@ -958,7 +972,10 @@ async function updateEntityFeaturedImage(
}
}
} 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);
if (error) {
logger.error('Error rejecting item', { error, itemId });
throw error;
}
if (error) {
handleNonCriticalError(error, {
action: 'Reject Submission Item',
metadata: { itemId }
});
throw error;
}
});
await Promise.all(updates);
@@ -1171,7 +1191,10 @@ async function updateSubmissionStatusAfterRejection(submissionId: string): Promi
.eq('submission_id', submissionId);
if (fetchError) {
logger.error('Error fetching submission items', { error: fetchError, submissionId });
handleNonCriticalError(fetchError, {
action: 'Fetch Submission Items for Status Update',
metadata: { submissionId }
});
return;
}
@@ -1202,7 +1225,10 @@ async function updateSubmissionStatusAfterRejection(submissionId: string): Promi
.eq('id', submissionId);
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) {
logger.error('Failed to record edit history', {
itemId,
editorId: userId,
error: historyError.message,
handleNonCriticalError(historyError, {
action: 'Record Edit History',
metadata: { itemId, editorId: userId }
});
// Don't fail the whole operation if history tracking fails
}
@@ -1293,10 +1318,12 @@ export async function editSubmissionItem(
true // isEdit = true
);
} catch (versionError) {
logger.error('Failed to create version for manual edit', {
action: 'create_version_for_edit',
itemType: currentItem.item_type,
entityId: currentItem.approved_entity_id
handleNonCriticalError(versionError, {
action: 'Create Version for Manual Edit',
metadata: {
itemType: currentItem.item_type,
entityId: currentItem.approved_entity_id
}
});
// Don't fail the entire operation, just log the error
// The edit itself is still saved, just without version history
@@ -1390,7 +1417,10 @@ export async function escalateSubmission(
}
});
} 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 || [];
} catch (error: unknown) {
logger.error('Error fetching edit history', {
itemId,
error: getErrorMessage(error),
handleNonCriticalError(error, {
action: 'Fetch Edit History',
metadata: { itemId }
});
return [];
}
@@ -1476,9 +1506,9 @@ export async function checkSubmissionConflict(
},
};
} catch (error: unknown) {
logger.error('Error checking submission conflict', {
submissionId,
error: getErrorMessage(error),
handleNonCriticalError(error, {
action: 'Check Submission Conflict',
metadata: { submissionId }
});
throw error;
}
@@ -1526,9 +1556,9 @@ export async function fetchSubmissionVersions(
return data || [];
} catch (error: unknown) {
logger.error('Error fetching submission versions', {
submissionId,
error: getErrorMessage(error),
handleNonCriticalError(error, {
action: 'Fetch Submission Versions',
metadata: { submissionId }
});
return [];
}