mirror of
				https://github.com/immich-app/immich.git
				synced 2025-10-31 02:27:08 -04:00 
			
		
		
		
	refactor: users.total metric (#23158)
* refactor: users.total metric * fix: broken test
This commit is contained in:
		
							parent
							
								
									0b941d78c4
								
							
						
					
					
						commit
						a70843e2b4
					
				| @ -1,4 +1,5 @@ | ||||
| import { | ||||
|   JobName, | ||||
|   LoginResponseDto, | ||||
|   createStack, | ||||
|   deleteUserAdmin, | ||||
| @ -327,6 +328,8 @@ describe('/admin/users', () => { | ||||
|         { headers: asBearerAuth(user.accessToken) }, | ||||
|       ); | ||||
| 
 | ||||
|       await utils.waitForQueueFinish(admin.accessToken, JobName.BackgroundTask); | ||||
| 
 | ||||
|       const { status, body } = await request(app) | ||||
|         .delete(`/admin/users/${user.userId}`) | ||||
|         .send({ force: true }) | ||||
|  | ||||
| @ -19,7 +19,6 @@ import { ConfigRepository } from 'src/repositories/config.repository'; | ||||
| import { EventRepository } from 'src/repositories/event.repository'; | ||||
| import { LoggingRepository } from 'src/repositories/logging.repository'; | ||||
| import { teardownTelemetry, TelemetryRepository } from 'src/repositories/telemetry.repository'; | ||||
| import { UserRepository } from 'src/repositories/user.repository'; | ||||
| import { services } from 'src/services'; | ||||
| import { AuthService } from 'src/services/auth.service'; | ||||
| import { CliService } from 'src/services/cli.service'; | ||||
| @ -56,7 +55,6 @@ class BaseModule implements OnModuleInit, OnModuleDestroy { | ||||
|     private jobService: JobService, | ||||
|     private telemetryRepository: TelemetryRepository, | ||||
|     private authService: AuthService, | ||||
|     private userRepository: UserRepository, | ||||
|   ) { | ||||
|     logger.setAppName(this.worker); | ||||
|   } | ||||
|  | ||||
| @ -17,7 +17,7 @@ import { AuthDto } from 'src/dtos/auth.dto'; | ||||
| import { NotificationDto } from 'src/dtos/notification.dto'; | ||||
| import { ReleaseNotification, ServerVersionResponseDto } from 'src/dtos/server.dto'; | ||||
| import { SyncAssetExifV1, SyncAssetV1 } from 'src/dtos/sync.dto'; | ||||
| import { ImmichWorker, MetadataKey, QueueName } from 'src/enum'; | ||||
| import { ImmichWorker, MetadataKey, QueueName, UserAvatarColor, UserStatus } from 'src/enum'; | ||||
| import { ConfigRepository } from 'src/repositories/config.repository'; | ||||
| import { LoggingRepository } from 'src/repositories/logging.repository'; | ||||
| import { JobItem, JobSource } from 'src/types'; | ||||
| @ -82,11 +82,33 @@ type EventMap = { | ||||
| 
 | ||||
|   // user events
 | ||||
|   UserSignup: [{ notify: boolean; id: string; password?: string }]; | ||||
|   UserCreate: [UserEvent]; | ||||
|   UserDelete: [UserEvent]; | ||||
|   UserRestore: [UserEvent]; | ||||
| 
 | ||||
|   // websocket events
 | ||||
|   WebsocketConnect: [{ userId: string }]; | ||||
| }; | ||||
| 
 | ||||
| type UserEvent = { | ||||
|   name: string; | ||||
|   id: string; | ||||
|   createdAt: Date; | ||||
|   updatedAt: Date; | ||||
|   deletedAt: Date | null; | ||||
|   status: UserStatus; | ||||
|   email: string; | ||||
|   profileImagePath: string; | ||||
|   isAdmin: boolean; | ||||
|   shouldChangePassword: boolean; | ||||
|   avatarColor: UserAvatarColor | null; | ||||
|   oauthId: string; | ||||
|   storageLabel: string | null; | ||||
|   quotaSizeInBytes: number | null; | ||||
|   quotaUsageInBytes: number; | ||||
|   profileChangedAt: Date; | ||||
| }; | ||||
| 
 | ||||
| export const serverEvents = ['ConfigUpdate'] as const; | ||||
| export type ServerEvents = (typeof serverEvents)[number]; | ||||
| 
 | ||||
|  | ||||
| @ -198,8 +198,8 @@ export class BaseService { | ||||
|   } | ||||
| 
 | ||||
|   async createUser(dto: Insertable<UserTable> & { email: string }): Promise<UserAdmin> { | ||||
|     const user = await this.userRepository.getByEmail(dto.email); | ||||
|     if (user) { | ||||
|     const exists = await this.userRepository.getByEmail(dto.email); | ||||
|     if (exists) { | ||||
|       throw new BadRequestException('User exists'); | ||||
|     } | ||||
| 
 | ||||
| @ -218,7 +218,10 @@ export class BaseService { | ||||
|       payload.storageLabel = sanitize(payload.storageLabel.replaceAll('.', '')); | ||||
|     } | ||||
| 
 | ||||
|     this.telemetryRepository.api.addToGauge(`immich.users.total`, 1); | ||||
|     return this.userRepository.create(payload); | ||||
|     const user = await this.userRepository.create(payload); | ||||
| 
 | ||||
|     await this.eventRepository.emit('UserCreate', user); | ||||
| 
 | ||||
|     return user; | ||||
|   } | ||||
| } | ||||
|  | ||||
| @ -34,6 +34,7 @@ import { SyncService } from 'src/services/sync.service'; | ||||
| import { SystemConfigService } from 'src/services/system-config.service'; | ||||
| import { SystemMetadataService } from 'src/services/system-metadata.service'; | ||||
| import { TagService } from 'src/services/tag.service'; | ||||
| import { TelemetryService } from 'src/services/telemetry.service'; | ||||
| import { TimelineService } from 'src/services/timeline.service'; | ||||
| import { TrashService } from 'src/services/trash.service'; | ||||
| import { UserAdminService } from 'src/services/user-admin.service'; | ||||
| @ -78,6 +79,7 @@ export const services = [ | ||||
|   SystemConfigService, | ||||
|   SystemMetadataService, | ||||
|   TagService, | ||||
|   TelemetryService, | ||||
|   TimelineService, | ||||
|   TrashService, | ||||
|   UserAdminService, | ||||
|  | ||||
							
								
								
									
										26
									
								
								server/src/services/telemetry.service.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								server/src/services/telemetry.service.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,26 @@ | ||||
| import { OnEvent } from 'src/decorators'; | ||||
| import { ImmichWorker } from 'src/enum'; | ||||
| import { BaseService } from 'src/services/base.service'; | ||||
| 
 | ||||
| export class TelemetryService extends BaseService { | ||||
|   @OnEvent({ name: 'AppBootstrap', workers: [ImmichWorker.Api] }) | ||||
|   async onBootstrap(): Promise<void> { | ||||
|     const userCount = await this.userRepository.getCount(); | ||||
|     this.telemetryRepository.api.addToGauge('immich.users.total', userCount); | ||||
|   } | ||||
| 
 | ||||
|   @OnEvent({ name: 'UserCreate' }) | ||||
|   onUserCreate() { | ||||
|     this.telemetryRepository.api.addToGauge(`immich.users.total`, 1); | ||||
|   } | ||||
| 
 | ||||
|   @OnEvent({ name: 'UserDelete' }) | ||||
|   onUserDelete() { | ||||
|     this.telemetryRepository.api.addToGauge(`immich.users.total`, -1); | ||||
|   } | ||||
| 
 | ||||
|   @OnEvent({ name: 'UserRestore' }) | ||||
|   onUserRestore() { | ||||
|     this.telemetryRepository.api.addToGauge(`immich.users.total`, 1); | ||||
|   } | ||||
| } | ||||
| @ -103,7 +103,8 @@ export class UserAdminService extends BaseService { | ||||
| 
 | ||||
|     const status = force ? UserStatus.Removing : UserStatus.Deleted; | ||||
|     const user = await this.userRepository.update(id, { status, deletedAt: new Date() }); | ||||
|     this.telemetryRepository.api.addToGauge(`immich.users.total`, -1); | ||||
| 
 | ||||
|     await this.eventRepository.emit('UserDelete', user); | ||||
| 
 | ||||
|     if (force) { | ||||
|       await this.jobRepository.queue({ name: JobName.UserDelete, data: { id: user.id, force } }); | ||||
| @ -116,7 +117,7 @@ export class UserAdminService extends BaseService { | ||||
|     await this.findOrFail(id, { withDeleted: true }); | ||||
|     await this.albumRepository.restoreAll(id); | ||||
|     const user = await this.userRepository.restore(id); | ||||
|     this.telemetryRepository.api.addToGauge('immich.users.total', 1); | ||||
|     await this.eventRepository.emit('UserRestore', user); | ||||
|     return mapUserAdmin(user); | ||||
|   } | ||||
| 
 | ||||
|  | ||||
| @ -3,14 +3,14 @@ import { Updateable } from 'kysely'; | ||||
| import { DateTime } from 'luxon'; | ||||
| import { SALT_ROUNDS } from 'src/constants'; | ||||
| import { StorageCore } from 'src/cores/storage.core'; | ||||
| import { OnEvent, OnJob } from 'src/decorators'; | ||||
| import { OnJob } from 'src/decorators'; | ||||
| import { AuthDto } from 'src/dtos/auth.dto'; | ||||
| import { LicenseKeyDto, LicenseResponseDto } from 'src/dtos/license.dto'; | ||||
| import { OnboardingDto, OnboardingResponseDto } from 'src/dtos/onboarding.dto'; | ||||
| import { UserPreferencesResponseDto, UserPreferencesUpdateDto, mapPreferences } from 'src/dtos/user-preferences.dto'; | ||||
| import { CreateProfileImageResponseDto } from 'src/dtos/user-profile.dto'; | ||||
| import { UserAdminResponseDto, UserResponseDto, UserUpdateMeDto, mapUser, mapUserAdmin } from 'src/dtos/user.dto'; | ||||
| import { CacheControl, ImmichWorker, JobName, JobStatus, QueueName, StorageFolder, UserMetadataKey } from 'src/enum'; | ||||
| import { CacheControl, JobName, JobStatus, QueueName, StorageFolder, UserMetadataKey } from 'src/enum'; | ||||
| import { UserFindOptions } from 'src/repositories/user.repository'; | ||||
| import { UserTable } from 'src/schema/tables/user.table'; | ||||
| import { BaseService } from 'src/services/base.service'; | ||||
| @ -213,12 +213,6 @@ export class UserService extends BaseService { | ||||
|     }; | ||||
|   } | ||||
| 
 | ||||
|   @OnEvent({ name: 'AppBootstrap', workers: [ImmichWorker.Api] }) | ||||
|   async onBootstrap(): Promise<void> { | ||||
|     const userCount = await this.userRepository.getCount(); | ||||
|     this.telemetryRepository.api.addToGauge('immich.users.total', userCount); | ||||
|   } | ||||
| 
 | ||||
|   @OnJob({ name: JobName.UserSyncUsage, queue: QueueName.BackgroundTask }) | ||||
|   async handleUserSyncUsage(): Promise<JobStatus> { | ||||
|     await this.userRepository.syncUsage(); | ||||
|  | ||||
| @ -44,7 +44,8 @@ beforeAll(async () => { | ||||
| describe(AuthService.name, () => { | ||||
|   describe('adminSignUp', () => { | ||||
|     it(`should sign up the admin`, async () => { | ||||
|       const { sut } = setup(); | ||||
|       const { sut, ctx } = setup(); | ||||
|       ctx.getMock(EventRepository).emit.mockResolvedValue(); | ||||
|       const dto = { name: 'Admin', email: 'admin@immich.cloud', password: 'password' }; | ||||
| 
 | ||||
|       await expect(sut.adminSignUp(dto)).resolves.toEqual( | ||||
|  | ||||
| @ -3,10 +3,10 @@ import { DateTime } from 'luxon'; | ||||
| import { ImmichEnvironment, JobName, JobStatus } from 'src/enum'; | ||||
| import { ConfigRepository } from 'src/repositories/config.repository'; | ||||
| import { CryptoRepository } from 'src/repositories/crypto.repository'; | ||||
| import { EventRepository } from 'src/repositories/event.repository'; | ||||
| import { JobRepository } from 'src/repositories/job.repository'; | ||||
| import { LoggingRepository } from 'src/repositories/logging.repository'; | ||||
| import { SystemMetadataRepository } from 'src/repositories/system-metadata.repository'; | ||||
| import { TelemetryRepository } from 'src/repositories/telemetry.repository'; | ||||
| import { UserRepository } from 'src/repositories/user.repository'; | ||||
| import { DB } from 'src/schema'; | ||||
| import { UserService } from 'src/services/user.service'; | ||||
| @ -22,7 +22,7 @@ const setup = (db?: Kysely<DB>) => { | ||||
|   return newMediumService(UserService, { | ||||
|     database: db || defaultDatabase, | ||||
|     real: [CryptoRepository, ConfigRepository, SystemMetadataRepository, UserRepository], | ||||
|     mock: [LoggingRepository, JobRepository, TelemetryRepository], | ||||
|     mock: [LoggingRepository, JobRepository, EventRepository], | ||||
|   }); | ||||
| }; | ||||
| 
 | ||||
| @ -35,7 +35,8 @@ beforeAll(async () => { | ||||
| describe(UserService.name, () => { | ||||
|   describe('create', () => { | ||||
|     it('should create a user', async () => { | ||||
|       const { sut } = setup(); | ||||
|       const { sut, ctx } = setup(); | ||||
|       ctx.getMock(EventRepository).emit.mockResolvedValue(); | ||||
|       const user = mediumFactory.userInsert(); | ||||
|       await expect(sut.createUser({ name: user.name, email: user.email })).resolves.toEqual( | ||||
|         expect.objectContaining({ name: user.name, email: user.email }), | ||||
| @ -43,14 +44,16 @@ describe(UserService.name, () => { | ||||
|     }); | ||||
| 
 | ||||
|     it('should reject user with duplicate email', async () => { | ||||
|       const { sut } = setup(); | ||||
|       const { sut, ctx } = setup(); | ||||
|       ctx.getMock(EventRepository).emit.mockResolvedValue(); | ||||
|       const user = mediumFactory.userInsert(); | ||||
|       await expect(sut.createUser({ email: user.email })).resolves.toMatchObject({ email: user.email }); | ||||
|       await expect(sut.createUser({ email: user.email })).rejects.toThrow('User exists'); | ||||
|     }); | ||||
| 
 | ||||
|     it('should not return password', async () => { | ||||
|       const { sut } = setup(); | ||||
|       const { sut, ctx } = setup(); | ||||
|       ctx.getMock(EventRepository).emit.mockResolvedValue(); | ||||
|       const dto = mediumFactory.userInsert({ password: 'password' }); | ||||
|       const user = await sut.createUser({ email: dto.email, password: 'password' }); | ||||
|       expect((user as any).password).toBeUndefined(); | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user