feat: Implement comprehensive request tracking and state management

This commit is contained in:
gpt-engineer-app[bot]
2025-10-21 12:51:44 +00:00
parent 12433e49e3
commit 74860c6774
8 changed files with 400 additions and 77 deletions

View File

@@ -0,0 +1,114 @@
/**
* Edge Function Request Tracking Wrapper
*
* Wraps Supabase function invocations with request tracking for debugging and monitoring.
* Provides correlation IDs for tracing requests across the system.
*/
import { supabase } from '@/integrations/supabase/client';
import { trackRequest } from './requestTracking';
import { getErrorMessage } from './errorHandler';
/**
* Invoke a Supabase edge function with request tracking
*
* @param functionName - Name of the edge function to invoke
* @param payload - Request payload
* @param userId - User ID for tracking (optional)
* @param parentRequestId - Parent request ID for chaining (optional)
* @param traceId - Trace ID for distributed tracing (optional)
* @returns Response data with requestId
*/
export async function invokeWithTracking<T = any>(
functionName: string,
payload: Record<string, unknown> = {},
userId?: string,
parentRequestId?: string,
traceId?: string
): Promise<{ data: T | null; error: any; requestId: string; duration: number }> {
try {
const { result, requestId, duration } = await trackRequest(
{
endpoint: `/functions/${functionName}`,
method: 'POST',
userId,
parentRequestId,
traceId,
},
async (context) => {
// Include client request ID in payload for correlation
const { data, error } = await supabase.functions.invoke<T>(functionName, {
body: { ...payload, clientRequestId: context.requestId },
});
if (error) throw error;
return data;
}
);
return { data: result, error: null, requestId, duration };
} catch (error: unknown) {
const errorMessage = getErrorMessage(error);
// On error, we don't have tracking info, so create basic response
return {
data: null,
error: { message: errorMessage },
requestId: 'unknown',
duration: 0,
};
}
}
/**
* Invoke multiple edge functions in parallel with batch tracking
*
* Uses a shared trace ID to correlate all operations.
*
* @param operations - Array of function invocation configurations
* @param userId - User ID for tracking
* @returns Array of results with their request IDs
*/
export async function invokeBatchWithTracking<T = any>(
operations: Array<{
functionName: string;
payload: Record<string, unknown>;
}>,
userId?: string
): Promise<
Array<{
functionName: string;
data: T | null;
error: any;
requestId: string;
duration: number;
}>
> {
const traceId = crypto.randomUUID();
const results = await Promise.allSettled(
operations.map(async (op) => {
const result = await invokeWithTracking<T>(
op.functionName,
op.payload,
userId,
undefined,
traceId
);
return { functionName: op.functionName, ...result };
})
);
return results.map((result, index) => {
if (result.status === 'fulfilled') {
return result.value;
} else {
return {
functionName: operations[index].functionName,
data: null,
error: { message: result.reason?.message || 'Unknown error' },
requestId: 'unknown',
duration: 0,
};
}
});
}

View File

@@ -1,5 +1,7 @@
import { supabase } from '@/integrations/supabase/client';
import { invokeWithTracking } from './edgeFunctionTracking';
import type { UploadedImage } from '@/components/upload/EntityMultiImageUploader';
import { logger } from './logger';
export interface CloudflareUploadResponse {
result: {
@@ -25,16 +27,28 @@ export async function uploadPendingImages(images: UploadedImage[]): Promise<Uplo
if (image.isLocal && image.file) {
const fileName = image.file.name;
// Step 1: Get upload URL from our Supabase Edge Function
const { data: uploadUrlData, error: urlError } = await supabase.functions.invoke('upload-image', {
body: { action: 'get-upload-url' }
});
// Step 1: Get upload URL from our Supabase Edge Function (with tracking)
const { data: uploadUrlData, error: urlError, requestId } = await invokeWithTracking(
'upload-image',
{ action: 'get-upload-url' }
);
if (urlError || !uploadUrlData?.uploadURL) {
console.error(`imageUploadHelper.uploadPendingImages: Failed to get upload URL for "${fileName}":`, urlError);
logger.error('Failed to get upload URL', {
action: 'upload_pending_images',
fileName,
requestId,
error: urlError?.message || 'Unknown error',
});
throw new Error(`Failed to get upload URL for "${fileName}": ${urlError?.message || 'Unknown error'}`);
}
logger.info('Got upload URL', {
action: 'upload_pending_images',
fileName,
requestId,
});
// Step 2: Upload file directly to Cloudflare
const formData = new FormData();
formData.append('file', image.file);
@@ -46,17 +60,31 @@ export async function uploadPendingImages(images: UploadedImage[]): Promise<Uplo
if (!uploadResponse.ok) {
const errorText = await uploadResponse.text();
console.error(`imageUploadHelper.uploadPendingImages: Upload failed for "${fileName}" (status ${uploadResponse.status}):`, errorText);
logger.error('Cloudflare upload failed', {
action: 'upload_pending_images',
fileName,
status: uploadResponse.status,
error: errorText,
});
throw new Error(`Upload failed for "${fileName}" (status ${uploadResponse.status}): ${errorText}`);
}
const result: CloudflareUploadResponse = await uploadResponse.json();
if (!result.success || !result.result) {
console.error(`imageUploadHelper.uploadPendingImages: Cloudflare upload unsuccessful for "${fileName}"`);
logger.error('Cloudflare upload unsuccessful', {
action: 'upload_pending_images',
fileName,
});
throw new Error(`Cloudflare upload returned unsuccessful response for "${fileName}"`);
}
logger.info('Image uploaded successfully', {
action: 'upload_pending_images',
fileName,
imageId: result.result.id,
});
// Clean up object URL
URL.revokeObjectURL(image.url);
@@ -106,13 +134,18 @@ 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) {
console.error(`imageUploadHelper.uploadPendingImages: Some uploads failed. Cleaning up ${newlyUploadedImageIds.length} newly uploaded images...`);
logger.error('Some uploads failed, cleaning up', {
action: 'upload_pending_images',
newlyUploadedCount: newlyUploadedImageIds.length,
failureCount: errors.length,
});
// Attempt cleanup in parallel with detailed error tracking
const cleanupResults = await Promise.allSettled(
newlyUploadedImageIds.map(imageId =>
supabase.functions.invoke('upload-image', {
body: { action: 'delete', imageId }
invokeWithTracking('upload-image', {
action: 'delete',
imageId,
})
)
);
@@ -120,13 +153,17 @@ export async function uploadPendingImages(images: UploadedImage[]): Promise<Uplo
// Track cleanup failures for better debugging
const cleanupFailures = cleanupResults.filter(r => r.status === 'rejected');
if (cleanupFailures.length > 0) {
console.error(
`imageUploadHelper.uploadPendingImages: Failed to cleanup ${cleanupFailures.length} of ${newlyUploadedImageIds.length} images.`,
'These images may remain orphaned in Cloudflare:',
newlyUploadedImageIds.filter((_, i) => cleanupResults[i].status === 'rejected')
);
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 {
console.log(`imageUploadHelper.uploadPendingImages: Successfully cleaned up ${newlyUploadedImageIds.length} images.`);
logger.info('Successfully cleaned up images', {
action: 'upload_pending_images_cleanup',
cleanedCount: newlyUploadedImageIds.length,
});
}
}

View File

@@ -11,6 +11,7 @@ import { createTableQuery } from '@/lib/supabaseHelpers';
import type { ModerationItem } from '@/types/moderation';
import { logger } from '@/lib/logger';
import { getErrorMessage } from '@/lib/errorHandler';
import { invokeWithTracking, invokeBatchWithTracking } from '@/lib/edgeFunctionTracking';
/**
* Type-safe update data for review moderation
@@ -184,20 +185,32 @@ export async function approveSubmissionItems(
itemIds: string[]
): Promise<ModerationActionResult> {
try {
const { error: approvalError } = await supabase.functions.invoke(
const { data: approvalData, error: approvalError, requestId } = await invokeWithTracking(
'process-selective-approval',
{
body: {
itemIds,
submissionId,
},
}
);
if (approvalError) {
logger.error('Submission items approval failed via edge function', {
action: 'approve_submission_items',
submissionId,
itemCount: itemIds.length,
requestId,
error: approvalError.message,
});
throw new Error(`Failed to process submission items: ${approvalError.message}`);
}
logger.info('Submission items approved successfully', {
action: 'approve_submission_items',
submissionId,
itemCount: itemIds.length,
requestId,
});
return {
success: true,
message: `Successfully processed ${itemIds.length} item(s)`,
@@ -478,24 +491,28 @@ export async function deleteSubmission(
// Delete photos from Cloudflare
if (validImageIds.length > 0) {
const deletePromises = validImageIds.map(async imageId => {
try {
await supabase.functions.invoke('upload-image', {
method: 'DELETE',
body: { imageId },
});
} catch (photoDeleteError: unknown) {
const errorMessage = getErrorMessage(photoDeleteError);
logger.error('Photo deletion failed', {
action: 'delete_submission_photo',
imageId,
error: errorMessage
const deleteResults = await invokeBatchWithTracking(
validImageIds.map(imageId => ({
functionName: 'upload-image',
payload: { action: 'delete', imageId },
})),
undefined
);
// Count successful deletions
const successfulDeletions = deleteResults.filter(r => !r.error);
deletedPhotoCount = successfulDeletions.length;
// Log any failures
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),
});
}
});
await Promise.allSettled(deletePromises);
deletedPhotoCount = validImageIds.length;
}
}
}

View File

@@ -0,0 +1,108 @@
/**
* Moderation Lock Monitor
*
* Monitors lock expiry and provides automatic renewal prompts for moderators.
* Prevents loss of work due to expired locks.
*/
import { useEffect } from 'react';
import type { ModerationState } from '../moderationStateMachine';
import type { ModerationAction } from '../moderationStateMachine';
import { hasActiveLock, needsLockRenewal } from '../moderationStateMachine';
import { toast } from '@/hooks/use-toast';
import { supabase } from '@/integrations/supabase/client';
import { logger } from '../logger';
/**
* Hook to monitor lock status and warn about expiry
*
* @param state - Current moderation state
* @param dispatch - State machine dispatch function
* @param itemId - ID of the locked item (optional, for manual extension)
*/
export function useLockMonitor(
state: ModerationState,
dispatch: React.Dispatch<ModerationAction>,
itemId?: string
) {
useEffect(() => {
if (!hasActiveLock(state)) {
return;
}
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' });
// Show toast with extension option
toast({
title: 'Lock Expiring Soon',
description: 'Your lock on this submission will expire in less than 2 minutes. Click to extend.',
variant: 'default',
});
}
}, 30000); // Check every 30 seconds
return () => clearInterval(checkInterval);
}, [state, dispatch, itemId]);
}
/**
* Extend the lock on a submission
*
* @param submissionId - Submission ID
* @param dispatch - State machine dispatch function
*/
async function handleExtendLock(
submissionId: string,
dispatch: React.Dispatch<ModerationAction>
) {
try {
// Call Supabase to extend lock (assumes 15 minute extension)
const { error } = await supabase
.from('content_submissions')
.update({
locked_until: new Date(Date.now() + 15 * 60 * 1000).toISOString(),
})
.eq('id', submissionId);
if (error) throw error;
// Update state machine with new lock time
dispatch({
type: 'LOCK_ACQUIRED',
payload: { lockExpires: new Date(Date.now() + 15 * 60 * 1000).toISOString() },
});
toast({
title: 'Lock Extended',
description: 'You have 15 more minutes to complete your review.',
});
logger.info('Lock extended successfully', {
action: 'lock_extended',
submissionId,
});
} catch (error) {
logger.error('Failed to extend lock', {
action: 'extend_lock_error',
submissionId,
error: error instanceof Error ? error.message : String(error),
});
toast({
title: 'Extension Failed',
description: 'Could not extend lock. Please save your work and re-claim the item.',
variant: 'destructive',
});
}
}

View File

@@ -1,6 +1,6 @@
import { serve } from 'https://deno.land/std@0.168.0/http/server.ts';
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2';
import { edgeLogger } from '../_shared/logger.ts';
import { edgeLogger, startRequest, endRequest } from '../_shared/logger.ts';
const corsHeaders = {
'Access-Control-Allow-Origin': '*',
@@ -8,6 +8,8 @@ const corsHeaders = {
};
serve(async (req) => {
const tracking = startRequest();
if (req.method === 'OPTIONS') {
return new Response(null, { headers: corsHeaders });
}
@@ -35,7 +37,7 @@ serve(async (req) => {
throw new Error('Unauthorized');
}
edgeLogger.info('Cancelling deletion request', { action: 'cancel_deletion', userId: user.id });
edgeLogger.info('Cancelling deletion request', { action: 'cancel_deletion', userId: user.id, requestId: tracking.requestId });
// Find pending deletion request
const { data: deletionRequest, error: requestError } = await supabaseClient
@@ -101,29 +103,34 @@ serve(async (req) => {
`,
}),
});
edgeLogger.info('Cancellation confirmation email sent', { action: 'cancel_deletion_email', userId: user.id });
edgeLogger.info('Cancellation confirmation email sent', { action: 'cancel_deletion_email', userId: user.id, requestId: tracking.requestId });
} catch (emailError) {
edgeLogger.error('Failed to send email', { action: 'cancel_deletion_email', userId: user.id });
edgeLogger.error('Failed to send email', { action: 'cancel_deletion_email', userId: user.id, requestId: tracking.requestId });
}
}
const duration = endRequest(tracking);
edgeLogger.info('Deletion cancelled successfully', { action: 'cancel_deletion_success', userId: user.id, requestId: tracking.requestId, duration });
return new Response(
JSON.stringify({
success: true,
message: 'Account deletion cancelled successfully',
requestId: tracking.requestId
}),
{
status: 200,
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
headers: { ...corsHeaders, 'Content-Type': 'application/json', 'X-Request-ID': tracking.requestId },
}
);
} catch (error) {
edgeLogger.error('Error cancelling deletion', { action: 'cancel_deletion_error', error: error instanceof Error ? error.message : String(error) });
const duration = endRequest(tracking);
edgeLogger.error('Error cancelling deletion', { action: 'cancel_deletion_error', error: error instanceof Error ? error.message : String(error), requestId: tracking.requestId, duration });
return new Response(
JSON.stringify({ error: error.message }),
JSON.stringify({ error: error.message, requestId: tracking.requestId }),
{
status: 400,
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
headers: { ...corsHeaders, 'Content-Type': 'application/json', 'X-Request-ID': tracking.requestId },
}
);
}

View File

@@ -1,15 +1,18 @@
import { serve } from "https://deno.land/std@0.168.0/http/server.ts";
import { Novu } from "npm:@novu/api@1.6.0";
// TODO: In production, restrict CORS to specific domains
// For now, allowing all origins for development flexibility
// Example production config: 'Access-Control-Allow-Origin': 'https://yourdomain.com'
const corsHeaders = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
};
// Simple request tracking
const startRequest = () => ({ requestId: crypto.randomUUID(), start: Date.now() });
const endRequest = (tracking: { start: number }) => Date.now() - tracking.start;
serve(async (req) => {
const tracking = startRequest();
if (req.method === 'OPTIONS') {
return new Response(null, { headers: corsHeaders });
}
@@ -197,28 +200,32 @@ serve(async (req) => {
data,
});
console.log('Subscriber created successfully:', subscriber.data);
const duration = endRequest(tracking);
console.log('Subscriber created successfully:', subscriber.data, { requestId: tracking.requestId, duration });
return new Response(
JSON.stringify({
success: true,
subscriberId: subscriber.data._id,
requestId: tracking.requestId
}),
{
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
headers: { ...corsHeaders, 'Content-Type': 'application/json', 'X-Request-ID': tracking.requestId },
status: 200,
}
);
} catch (error: any) {
console.error('Error creating Novu subscriber:', error);
const duration = endRequest(tracking);
console.error('Error creating Novu subscriber:', error, { requestId: tracking.requestId, duration });
return new Response(
JSON.stringify({
success: false,
error: error.message,
requestId: tracking.requestId
}),
{
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
headers: { ...corsHeaders, 'Content-Type': 'application/json', 'X-Request-ID': tracking.requestId },
status: 500,
}
);

View File

@@ -1,5 +1,6 @@
import "jsr:@supabase/functions-js/edge-runtime.d.ts";
import { createClient } from "https://esm.sh/@supabase/supabase-js@2.57.4";
import { startRequest, endRequest } from '../_shared/logger.ts';
const corsHeaders = {
'Access-Control-Allow-Origin': '*',
@@ -73,6 +74,8 @@ async function ensureUniqueUsername(
}
Deno.serve(async (req) => {
const tracking = startRequest();
if (req.method === 'OPTIONS') {
return new Response(null, { headers: corsHeaders });
}
@@ -188,9 +191,10 @@ Deno.serve(async (req) => {
.single();
if (profile?.avatar_image_id) {
console.log('[OAuth Profile] Avatar already exists, skipping');
return new Response(JSON.stringify({ success: true, message: 'Avatar already exists' }), {
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
const duration = endRequest(tracking);
console.log('[OAuth Profile] Avatar already exists, skipping', { requestId: tracking.requestId, duration });
return new Response(JSON.stringify({ success: true, message: 'Avatar already exists', requestId: tracking.requestId }), {
headers: { ...corsHeaders, 'Content-Type': 'application/json', 'X-Request-ID': tracking.requestId },
});
}
@@ -326,22 +330,27 @@ Deno.serve(async (req) => {
});
}
console.log('[OAuth Profile] Profile updated successfully');
console.log('[OAuth Profile] Profile updated successfully', { requestId: tracking.requestId });
}
const duration = endRequest(tracking);
console.log('[OAuth Profile] Processing complete', { requestId: tracking.requestId, duration });
return new Response(JSON.stringify({
success: true,
avatar_uploaded: !!cloudflareImageId,
profile_updated: Object.keys(updateData).length > 0,
requestId: tracking.requestId
}), {
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
headers: { ...corsHeaders, 'Content-Type': 'application/json', 'X-Request-ID': tracking.requestId },
});
} catch (error) {
console.error('[OAuth Profile] Error:', error);
return new Response(JSON.stringify({ error: error.message }), {
const duration = endRequest(tracking);
console.error('[OAuth Profile] Error:', error, { requestId: tracking.requestId, duration });
return new Response(JSON.stringify({ error: error.message, requestId: tracking.requestId }), {
status: 500,
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
headers: { ...corsHeaders, 'Content-Type': 'application/json', 'X-Request-ID': tracking.requestId },
});
}
});

View File

@@ -1,6 +1,6 @@
import { serve } from "https://deno.land/std@0.168.0/http/server.ts"
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2'
import { edgeLogger } from '../_shared/logger.ts'
import { edgeLogger, startRequest, endRequest } from '../_shared/logger.ts'
// Environment-aware CORS configuration
const getAllowedOrigin = (requestOrigin: string | null): string | null => {
@@ -70,12 +70,13 @@ const createAuthenticatedSupabaseClient = (authHeader: string) => {
}
serve(async (req) => {
const tracking = startRequest();
const requestOrigin = req.headers.get('origin');
const allowedOrigin = getAllowedOrigin(requestOrigin);
// Check if this is a CORS request with a disallowed origin
if (requestOrigin && !allowedOrigin) {
edgeLogger.warn('CORS request rejected', { action: 'cors_validation', origin: requestOrigin });
edgeLogger.warn('CORS request rejected', { action: 'cors_validation', origin: requestOrigin, requestId: tracking.requestId });
return new Response(
JSON.stringify({
error: 'Origin not allowed',
@@ -160,19 +161,26 @@ serve(async (req) => {
}
if (profile.banned) {
const duration = endRequest(tracking);
return new Response(
JSON.stringify({
error: 'Account suspended',
message: 'Account suspended. Contact support for assistance.'
message: 'Account suspended. Contact support for assistance.',
requestId: tracking.requestId
}),
{
status: 403,
headers: { ...corsHeaders, 'Content-Type': 'application/json' }
headers: {
...corsHeaders,
'Content-Type': 'application/json',
'X-Request-ID': tracking.requestId
}
}
)
}
// Delete image from Cloudflare
edgeLogger.info('Deleting image', { action: 'delete_image', requestId: tracking.requestId });
let requestBody;
try {
requestBody = await req.json();
@@ -280,10 +288,12 @@ serve(async (req) => {
)
}
const duration = endRequest(tracking);
edgeLogger.info('Image deleted successfully', { action: 'delete_image', requestId: tracking.requestId, duration });
return new Response(
JSON.stringify({ success: true, deleted: true }),
JSON.stringify({ success: true, deleted: true, requestId: tracking.requestId }),
{
headers: { ...corsHeaders, 'Content-Type': 'application/json' }
headers: { ...corsHeaders, 'Content-Type': 'application/json', 'X-Request-ID': tracking.requestId }
}
)
}
@@ -344,19 +354,22 @@ serve(async (req) => {
}
if (profile.banned) {
const duration = endRequest(tracking);
return new Response(
JSON.stringify({
error: 'Account suspended',
message: 'Account suspended. Contact support for assistance.'
message: 'Account suspended. Contact support for assistance.',
requestId: tracking.requestId
}),
{
status: 403,
headers: { ...corsHeaders, 'Content-Type': 'application/json' }
headers: { ...corsHeaders, 'Content-Type': 'application/json', 'X-Request-ID': tracking.requestId }
}
)
}
// Request a direct upload URL from Cloudflare
edgeLogger.info('Requesting upload URL', { action: 'request_upload_url', requestId: tracking.requestId });
let requestBody;
try {
requestBody = await req.json();
@@ -448,14 +461,17 @@ serve(async (req) => {
}
// Return the upload URL and image ID to the client
const duration = endRequest(tracking);
edgeLogger.info('Upload URL created', { action: 'upload_url_success', requestId: tracking.requestId, duration });
return new Response(
JSON.stringify({
success: true,
uploadURL: directUploadResult.result.uploadURL,
id: directUploadResult.result.id,
requestId: tracking.requestId
}),
{
headers: { ...corsHeaders, 'Content-Type': 'application/json' }
headers: { ...corsHeaders, 'Content-Type': 'application/json', 'X-Request-ID': tracking.requestId }
}
)
}
@@ -569,10 +585,13 @@ serve(async (req) => {
// Return the image details with convenient URLs
const result = imageResult.result
const duration = endRequest(tracking);
// Construct proper imagedelivery.net URLs using account hash and image ID
const baseUrl = `https://imagedelivery.net/${CLOUDFLARE_ACCOUNT_HASH}/${result.id}`
edgeLogger.info('Image status retrieved', { action: 'get_image_status', requestId: tracking.requestId, duration });
return new Response(
JSON.stringify({
success: true,
@@ -587,36 +606,41 @@ serve(async (req) => {
medium: `${baseUrl}/medium`,
large: `${baseUrl}/large`,
avatar: `${baseUrl}/avatar`,
} : null
} : null,
requestId: tracking.requestId
}),
{
headers: { ...corsHeaders, 'Content-Type': 'application/json' }
headers: { ...corsHeaders, 'Content-Type': 'application/json', 'X-Request-ID': tracking.requestId }
}
)
}
const duration = endRequest(tracking);
return new Response(
JSON.stringify({
error: 'Method not allowed',
message: 'HTTP method not supported for this endpoint'
message: 'HTTP method not supported for this endpoint',
requestId: tracking.requestId
}),
{
status: 405,
headers: { ...corsHeaders, 'Content-Type': 'application/json' }
headers: { ...corsHeaders, 'Content-Type': 'application/json', 'X-Request-ID': tracking.requestId }
}
)
} catch (error: unknown) {
const duration = endRequest(tracking);
const errorMessage = error instanceof Error ? error.message : 'An unexpected error occurred';
console.error('[Upload] Error:', { error: errorMessage });
edgeLogger.error('Upload function error', { action: 'upload_error', requestId: tracking.requestId, duration, error: errorMessage });
return new Response(
JSON.stringify({
error: 'Internal server error',
message: errorMessage
message: errorMessage,
requestId: tracking.requestId
}),
{
status: 500,
headers: { ...corsHeaders, 'Content-Type': 'application/json' }
headers: { ...corsHeaders, 'Content-Type': 'application/json', 'X-Request-ID': tracking.requestId }
}
)
}