diff --git a/app/(chat)/api/files/direct-upload/route.ts b/app/(chat)/api/files/direct-upload/route.ts index dff8785bd..af795c131 100644 --- a/app/(chat)/api/files/direct-upload/route.ts +++ b/app/(chat)/api/files/direct-upload/route.ts @@ -29,6 +29,10 @@ const DEFAULT_PART_SIZE_BYTES = 8 * 1024 * 1024; const PRESIGNED_URL_EXPIRES_SECONDS = 15 * 60; const DIRECT_UPLOAD_PREFIX = "uploads/direct"; +function elapsedMs(startedAt: number) { + return Date.now() - startedAt; +} + const allowedDirectUploadTypes = new Set([ "image/jpeg", "image/jpg", @@ -141,6 +145,7 @@ async function createPartUrls(params: { } export async function POST(request: Request) { + const requestStartedAt = Date.now(); const session = await auth(); if (!session?.user?.id) { return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); @@ -159,6 +164,16 @@ export async function POST(request: Request) { ); } + logger.info("direct_upload_request_start", { + action: payload.action, + size: "size" in payload ? payload.size : null, + contentType: "contentType" in payload ? payload.contentType : null, + purpose: "purpose" in payload ? payload.purpose || null : null, + clientUploadId: + "clientUploadId" in payload ? payload.clientUploadId || null : null, + }); + + const resolveStartedAt = Date.now(); const target = await resolveS3UploadTarget().catch((error) => { logger.error("resolve_storage_target_failed", error); return null; @@ -170,6 +185,13 @@ export async function POST(request: Request) { { status: 409 } ); } + logger.info("direct_upload_storage_target_resolved", { + action: payload.action, + elapsedMs: elapsedMs(resolveStartedAt), + provider: target.config.provider || "s3", + bucket: target.config.bucket, + hasPublicUrl: Boolean(target.config.publicUrl), + }); const client = createS3Client(target.config); const bucket = target.config.bucket; @@ -203,6 +225,7 @@ export async function POST(request: Request) { ); if (payload.size < MULTIPART_THRESHOLD_BYTES) { + const presignStartedAt = Date.now(); const command = new PutObjectCommand({ Bucket: bucket, Key: key, @@ -213,6 +236,13 @@ export async function POST(request: Request) { const url = await getSignedUrl(client, command, { expiresIn: PRESIGNED_URL_EXPIRES_SECONDS, }); + logger.info("direct_upload_single_presigned", { + elapsedMs: elapsedMs(presignStartedAt), + totalElapsedMs: elapsedMs(requestStartedAt), + size: payload.size, + contentType, + clientUploadId: payload.clientUploadId || null, + }); const headers: Record = { "Content-Type": contentType, @@ -232,6 +262,7 @@ export async function POST(request: Request) { }); } + const multipartStartedAt = Date.now(); const createResult = await client.send( new CreateMultipartUploadCommand({ Bucket: bucket, @@ -256,6 +287,15 @@ export async function POST(request: Request) { uploadId, partCount, }); + logger.info("direct_upload_multipart_prepared", { + elapsedMs: elapsedMs(multipartStartedAt), + totalElapsedMs: elapsedMs(requestStartedAt), + size: payload.size, + partCount, + partSize, + contentType, + clientUploadId: payload.clientUploadId || null, + }); return NextResponse.json({ mode: "multipart", @@ -271,6 +311,7 @@ export async function POST(request: Request) { if (payload.action === "abort") { const key = assertUserOwnedKey(payload.key, session.user.id); + const abortStartedAt = Date.now(); await client.send( new AbortMultipartUploadCommand({ Bucket: bucket, @@ -278,6 +319,11 @@ export async function POST(request: Request) { UploadId: payload.uploadId, }) ); + logger.info("direct_upload_multipart_aborted", { + elapsedMs: elapsedMs(abortStartedAt), + totalElapsedMs: elapsedMs(requestStartedAt), + clientUploadId: payload.clientUploadId || null, + }); return NextResponse.json({ ok: true }); } @@ -291,6 +337,7 @@ export async function POST(request: Request) { } if (payload.uploadId) { + const completeMultipartStartedAt = Date.now(); const parts = [...(payload.parts || [])].sort( (a, b) => a.PartNumber - b.PartNumber ); @@ -311,14 +358,25 @@ export async function POST(request: Request) { }, }) ); + logger.info("direct_upload_multipart_completed", { + elapsedMs: elapsedMs(completeMultipartStartedAt), + partCount: parts.length, + clientUploadId: payload.clientUploadId || null, + }); } + const headStartedAt = Date.now(); const head = await client.send( new HeadObjectCommand({ Bucket: bucket, Key: key, }) ); + logger.info("direct_upload_head_object_completed", { + elapsedMs: elapsedMs(headStartedAt), + uploadMode: payload.uploadId ? "multipart" : "single", + clientUploadId: payload.clientUploadId || null, + }); const actualSize = typeof head.ContentLength === "number" ? head.ContentLength : null; @@ -329,6 +387,7 @@ export async function POST(request: Request) { const url = buildS3PublicUrl(target.config, key); const resolvedContentType = head.ContentType || contentType || getContentType(payload.filename); + const assetStartedAt = Date.now(); const asset = await upsertMediaAsset({ userId: session.user.id, mediaType: "image", @@ -349,6 +408,13 @@ export async function POST(request: Request) { clientUploadId: payload.clientUploadId || null, }, }); + logger.info("direct_upload_asset_registered", { + elapsedMs: elapsedMs(assetStartedAt), + totalElapsedMs: elapsedMs(requestStartedAt), + uploadMode: payload.uploadId ? "multipart" : "single", + assetId: asset?.id ?? null, + clientUploadId: payload.clientUploadId || null, + }); await maybeEnqueueStorageMigrationTask({ uploaded: { url, pathname: key }, diff --git a/lib/client/upload-with-retry.ts b/lib/client/upload-with-retry.ts index b18bb82c6..095bf703c 100644 --- a/lib/client/upload-with-retry.ts +++ b/lib/client/upload-with-retry.ts @@ -7,6 +7,7 @@ const SERVER_UPLOAD_MAX_BYTES = 50 * 1024 * 1024; const DIRECT_UPLOAD_PART_CONCURRENCY = 4; const DIRECT_UPLOAD_SUPPRESS_MS = 2 * 60 * 1000; const CLIENT_UPLOAD_ID_FIELD = "clientUploadId"; +const DIRECT_UPLOAD_INITIATE_TIMEOUT_MS = 3 * 1000; const DIRECT_UPLOAD_ACTION_TIMEOUT_MS = 15 * 1000; const DIRECT_UPLOAD_ABORT_TIMEOUT_MS = 8 * 1000; const XHR_UPLOAD_TOTAL_TIMEOUT_MS = 6 * 60 * 1000; @@ -725,7 +726,8 @@ async function fetchDirectUpload( purpose: file.purpose, clientUploadId: file.clientUploadId, }, - signal + signal, + DIRECT_UPLOAD_INITIATE_TIMEOUT_MS ); if (upload.mode === "single") {