telemetry module (#934)

* update lockfile

* fix color check

* telemetry

* complete

* Use interval
This commit is contained in:
Philip Okugbe
2025-03-23 13:12:41 +00:00
committed by GitHub
parent 593f41a050
commit 13039cfacc
6 changed files with 140 additions and 0 deletions

View File

@ -46,6 +46,7 @@
"@nestjs/passport": "^11.0.5",
"@nestjs/platform-fastify": "^11.0.10",
"@nestjs/platform-socket.io": "^11.0.10",
"@nestjs/schedule": "^5.0.1",
"@nestjs/terminus": "^11.0.0",
"@nestjs/websockets": "^11.0.10",
"@node-saml/passport-saml": "^5.0.1",

View File

@ -15,6 +15,7 @@ import { HealthModule } from './integrations/health/health.module';
import { ExportModule } from './integrations/export/export.module';
import { ImportModule } from './integrations/import/import.module';
import { SecurityModule } from './integrations/security/security.module';
import { TelemetryModule } from './integrations/telemetry/telemetry.module';
const enterpriseModules = [];
try {
@ -50,6 +51,7 @@ try {
}),
EventEmitterModule.forRoot(),
SecurityModule,
TelemetryModule,
...enterpriseModules,
],
controllers: [AppController],

View File

@ -182,4 +182,11 @@ export class EnvironmentService {
.toLowerCase();
return isStandalone === 'true';
}
isDisableTelemetry(): boolean {
const disable = this.configService
.get<string>('DISABLE_TELEMETRY', 'false')
.toLowerCase();
return disable === 'true';
}
}

View File

@ -0,0 +1,9 @@
import { Module } from '@nestjs/common';
import { TelemetryService } from './telemetry.service';
import { ScheduleModule } from '@nestjs/schedule';
@Module({
providers: [TelemetryService],
imports: [ScheduleModule.forRoot()],
})
export class TelemetryModule {}

View File

@ -0,0 +1,87 @@
import { Injectable } from '@nestjs/common';
import { Interval, SchedulerRegistry } from '@nestjs/schedule';
import { EnvironmentService } from '../environment/environment.service';
import { InjectKysely } from 'nestjs-kysely';
import { KyselyDB } from '@docmost/db/types/kysely.types';
import { createHmac } from 'node:crypto';
import { WorkspaceRepo } from '@docmost/db/repos/workspace/workspace.repo';
// eslint-disable-next-line @typescript-eslint/no-require-imports
const packageJson = require('./../../../package.json');
@Injectable()
export class TelemetryService {
private readonly ENDPOINT_URL = 'https://tel.docmost.com/api/event';
constructor(
private readonly environmentService: EnvironmentService,
@InjectKysely() private readonly db: KyselyDB,
private readonly workspaceRepo: WorkspaceRepo,
private schedulerRegistry: SchedulerRegistry,
) {}
@Interval('telemetry', 24 * 60 * 60 * 1000)
async sendTelemetry() {
try {
if (
this.environmentService.isDisableTelemetry() ||
this.environmentService.isCloud() ||
this.environmentService.getNodeEnv() !== 'production'
) {
this.schedulerRegistry.deleteInterval('telemetry');
return;
}
const workspace = await this.workspaceRepo.findFirst();
if (!workspace) {
return;
}
const anonymizedHash = createHmac(
'sha256',
this.environmentService.getAppSecret(),
)
.update(workspace.id)
.digest('hex');
const { userCount } = await this.db
.selectFrom('users')
.select((eb) => eb.fn.count('id').as('userCount'))
.executeTakeFirst();
const { pageCount } = await this.db
.selectFrom('pages')
.select((eb) => eb.fn.count('id').as('pageCount'))
.executeTakeFirst();
const { workspaceCount } = await this.db
.selectFrom('workspaces')
.select((eb) => eb.fn.count('id').as('workspaceCount'))
.executeTakeFirst();
const { spaceCount } = await this.db
.selectFrom('spaces')
.select((eb) => eb.fn.count('id').as('spaceCount'))
.executeTakeFirst();
const data = {
instanceId: anonymizedHash,
version: packageJson.version,
userCount,
pageCount,
spaceCount,
workspaceCount,
};
await fetch(this.ENDPOINT_URL, {
method: 'POST',
headers: {
'User-Agent': 'docmost:' + data.version,
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
});
} catch (err) {
/* empty */
}
}
}