diff --git a/server/src/app.module.ts b/server/src/app.module.ts index 8d261463e7..a1cd1edfdf 100644 --- a/server/src/app.module.ts +++ b/server/src/app.module.ts @@ -19,6 +19,7 @@ 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'; @@ -55,6 +56,7 @@ class BaseModule implements OnModuleInit, OnModuleDestroy { private jobService: JobService, private telemetryRepository: TelemetryRepository, private authService: AuthService, + private userRepository: UserRepository, ) { logger.setAppName(this.worker); } diff --git a/server/src/queries/user.repository.sql b/server/src/queries/user.repository.sql index 6a02654781..c5a4f139a7 100644 --- a/server/src/queries/user.repository.sql +++ b/server/src/queries/user.repository.sql @@ -363,6 +363,14 @@ group by order by "user"."createdAt" asc +-- UserRepository.getCount +select + count(*) as "count" +from + "user" +where + "user"."deletedAt" is null + -- UserRepository.updateUsage update "user" set diff --git a/server/src/repositories/user.repository.ts b/server/src/repositories/user.repository.ts index 44f4a2bb9c..20b41c80f8 100644 --- a/server/src/repositories/user.repository.ts +++ b/server/src/repositories/user.repository.ts @@ -286,6 +286,16 @@ export class UserRepository { .execute(); } + @GenerateSql() + async getCount(): Promise { + const result = await this.db + .selectFrom('user') + .select((eb) => eb.fn.countAll().as('count')) + .where('user.deletedAt', 'is', null) + .executeTakeFirstOrThrow(); + return Number(result.count); + } + @GenerateSql({ params: [DummyValue.UUID, DummyValue.NUMBER] }) async updateUsage(id: string, delta: number): Promise { await this.db diff --git a/server/src/services/base.service.ts b/server/src/services/base.service.ts index 2f0e272883..6b85c3ec6c 100644 --- a/server/src/services/base.service.ts +++ b/server/src/services/base.service.ts @@ -215,6 +215,7 @@ export class BaseService { payload.storageLabel = sanitize(payload.storageLabel.replaceAll('.', '')); } + this.telemetryRepository.api.addToGauge(`immich.users.total`, 1); return this.userRepository.create(payload); } } diff --git a/server/src/services/user-admin.service.ts b/server/src/services/user-admin.service.ts index ce70419ff6..a57072e496 100644 --- a/server/src/services/user-admin.service.ts +++ b/server/src/services/user-admin.service.ts @@ -102,6 +102,7 @@ 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); if (force) { await this.jobRepository.queue({ name: JobName.UserDelete, data: { id: user.id, force } }); @@ -114,6 +115,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); return mapUserAdmin(user); } diff --git a/server/src/services/user.service.ts b/server/src/services/user.service.ts index 6849b17ac3..fc71777673 100644 --- a/server/src/services/user.service.ts +++ b/server/src/services/user.service.ts @@ -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 { OnJob } from 'src/decorators'; +import { OnEvent, 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, JobName, JobStatus, QueueName, StorageFolder, UserMetadataKey } from 'src/enum'; +import { CacheControl, ImmichWorker, 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,6 +213,12 @@ export class UserService extends BaseService { }; } + @OnEvent({ name: 'AppBootstrap', workers: [ImmichWorker.Api] }) + async onBootstrap(): Promise { + const userCount = await this.userRepository.getCount(); + this.telemetryRepository.api.addToGauge('immich.users.total', userCount); + } + @OnJob({ name: JobName.UserSyncUsage, queue: QueueName.BackgroundTask }) async handleUserSyncUsage(): Promise { await this.userRepository.syncUsage(); diff --git a/server/test/medium.factory.ts b/server/test/medium.factory.ts index a169d96322..c7356e2f1b 100644 --- a/server/test/medium.factory.ts +++ b/server/test/medium.factory.ts @@ -39,6 +39,7 @@ import { StorageRepository } from 'src/repositories/storage.repository'; import { SyncCheckpointRepository } from 'src/repositories/sync-checkpoint.repository'; import { SyncRepository } from 'src/repositories/sync.repository'; import { SystemMetadataRepository } from 'src/repositories/system-metadata.repository'; +import { TelemetryRepository } from 'src/repositories/telemetry.repository'; import { UserRepository } from 'src/repositories/user.repository'; import { VersionHistoryRepository } from 'src/repositories/version-history.repository'; import { DB } from 'src/schema'; @@ -54,6 +55,7 @@ import { StackTable } from 'src/schema/tables/stack.table'; import { UserTable } from 'src/schema/tables/user.table'; import { BASE_SERVICE_DEPENDENCIES, BaseService } from 'src/services/base.service'; import { SyncService } from 'src/services/sync.service'; +import { newTelemetryRepositoryMock } from 'test/repositories/telemetry.repository.mock'; import { factory, newDate, newEmbedding, newUuid } from 'test/small.factory'; import { automock, wait } from 'test/utils'; import { Mocked } from 'vitest'; @@ -347,6 +349,10 @@ const newMockRepository = (key: ClassConstructor) => { return automock(key); } + case TelemetryRepository: { + return newTelemetryRepositoryMock(); + } + case DatabaseRepository: { return automock(DatabaseRepository, { args: [undefined, { setContext: () => {} }, { getEnv: () => ({ database: { vectorExtension: '' } }) }], diff --git a/server/test/medium/specs/services/auth.service.spec.ts b/server/test/medium/specs/services/auth.service.spec.ts index 14ea1451f2..60d3210f4c 100644 --- a/server/test/medium/specs/services/auth.service.spec.ts +++ b/server/test/medium/specs/services/auth.service.spec.ts @@ -11,6 +11,7 @@ import { LoggingRepository } from 'src/repositories/logging.repository'; import { SessionRepository } from 'src/repositories/session.repository'; import { StorageRepository } from 'src/repositories/storage.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 { AuthService } from 'src/services/auth.service'; @@ -32,7 +33,7 @@ const setup = (db?: Kysely) => { SystemMetadataRepository, UserRepository, ], - mock: [LoggingRepository, StorageRepository, EventRepository], + mock: [LoggingRepository, StorageRepository, EventRepository, TelemetryRepository], }); }; diff --git a/server/test/medium/specs/services/user.service.spec.ts b/server/test/medium/specs/services/user.service.spec.ts index 7643be292e..0d72d39950 100644 --- a/server/test/medium/specs/services/user.service.spec.ts +++ b/server/test/medium/specs/services/user.service.spec.ts @@ -6,6 +6,7 @@ import { CryptoRepository } from 'src/repositories/crypto.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'; @@ -21,7 +22,7 @@ const setup = (db?: Kysely) => { return newMediumService(UserService, { database: db || defaultDatabase, real: [CryptoRepository, ConfigRepository, SystemMetadataRepository, UserRepository], - mock: [LoggingRepository, JobRepository], + mock: [LoggingRepository, JobRepository, TelemetryRepository], }); };