@@ -2,7 +2,8 @@ import { once } from 'events'
22import { FastifyRequest } from 'fastify'
33import { PassThrough , Readable } from 'stream'
44import { ErrorCode , isStorageError , StorageBackendError } from '../internal/errors'
5- import { ObjectAdminDelete } from '../storage/events'
5+ import { fileUploadedSuccess , fileUploadStarted } from '../internal/monitoring/metrics'
6+ import { ObjectAdminDelete , ObjectCreatedPostEvent } from '../storage/events'
67import { TenantLocation } from '../storage/locator'
78import { fileUploadFromRequest , Uploader } from '../storage/uploader'
89
@@ -21,6 +22,21 @@ function createUploader(
2122 new TenantLocation ( 'test-bucket' )
2223 )
2324}
25+
26+ function createUploaderDb ( overrides : Partial < UploaderDatabase > = { } ) {
27+ const db = {
28+ tenantId : 'stub-tenant' ,
29+ reqId : 'req-1' ,
30+ sbReqId : 'sb-req-1' ,
31+ tenant : ( ) => ( { ref : 'stub-tenant' , host : 'stub-tenant.local' } ) ,
32+ testPermission : vi . fn ( async ( ) => undefined ) ,
33+ ...overrides ,
34+ } as Partial < UploaderDatabase > &
35+ Pick < UploaderDatabase , 'tenantId' | 'reqId' | 'tenant' | 'testPermission' >
36+
37+ return db
38+ }
39+
2440describe ( 'fileUploadFromRequest' , ( ) => {
2541 test ( 'keeps multipart/form-data file size undefined even when the request content-length exceeds 5GB' , async ( ) => {
2642 const file = Readable . from ( [ 'payload' ] ) as Readable & { truncated : boolean }
@@ -380,3 +396,83 @@ describe('fileUploadFromRequest', () => {
380396 completeUploadSpy . mockRestore ( )
381397 } )
382398} )
399+
400+ describe ( 'Uploader metrics' , ( ) => {
401+ test ( 'prepareUpload records upload start attributes without tenant id labels' , async ( ) => {
402+ const addSpy = vi . spyOn ( fileUploadStarted , 'add' )
403+ const uploader = createUploader (
404+ {
405+ uploadObject : vi . fn ( ) ,
406+ } ,
407+ createUploaderDb ( )
408+ )
409+
410+ try {
411+ await uploader . prepareUpload ( {
412+ bucketId : 'bucket' ,
413+ objectName : 'test.txt' ,
414+ owner : undefined ,
415+ isUpsert : false ,
416+ userMetadata : undefined ,
417+ metadata : undefined ,
418+ uploadType : 'standard' ,
419+ } )
420+
421+ expect ( addSpy ) . toHaveBeenCalledWith ( 1 , { uploadType : 'standard' } )
422+ } finally {
423+ addSpy . mockRestore ( )
424+ }
425+ } )
426+
427+ test ( 'completeUpload records upload success attributes without tenant id labels' , async ( ) => {
428+ const addSpy = vi . spyOn ( fileUploadedSuccess , 'add' )
429+ const sendWebhookSpy = vi
430+ . spyOn ( ObjectCreatedPostEvent , 'sendWebhook' )
431+ . mockResolvedValue ( undefined )
432+ const transactionDb = {
433+ waitObjectLock : vi . fn ( ) . mockResolvedValue ( undefined ) ,
434+ findObject : vi . fn ( ) . mockResolvedValue ( undefined ) ,
435+ upsertObject : vi . fn ( ) . mockResolvedValue ( { id : 'object-id' } ) ,
436+ }
437+ const db = createUploaderDb ( {
438+ asSuperUser : vi . fn ( ) . mockReturnValue ( {
439+ connection : {
440+ setAbortSignal : vi . fn ( ) ,
441+ } ,
442+ withTransaction : vi . fn ( async ( fn ) => fn ( transactionDb ) ) ,
443+ } ) ,
444+ } )
445+ const uploader = createUploader (
446+ {
447+ uploadObject : vi . fn ( ) ,
448+ } ,
449+ db
450+ )
451+
452+ try {
453+ await uploader . completeUpload ( {
454+ version : 'version-1' ,
455+ bucketId : 'bucket' ,
456+ objectName : 'test.txt' ,
457+ owner : undefined ,
458+ objectMetadata : {
459+ eTag : '"etag"' ,
460+ mimetype : 'text/plain' ,
461+ cacheControl : 'max-age=3600' ,
462+ lastModified : new Date ( ) ,
463+ contentLength : 7 ,
464+ httpStatusCode : 200 ,
465+ size : 7 ,
466+ } ,
467+ uploadType : 'standard' ,
468+ isUpsert : false ,
469+ userMetadata : undefined ,
470+ } )
471+
472+ expect ( addSpy ) . toHaveBeenCalledWith ( 1 , { uploadType : 'standard' } )
473+ } finally {
474+ addSpy . mockRestore ( )
475+ sendWebhookSpy . mockRestore ( )
476+ }
477+ } )
478+ } )
0 commit comments