From 13039cfacc147e7e29790af75e30ed77a8ba4940 Mon Sep 17 00:00:00 2001 From: Philip Okugbe <16838612+Philipinho@users.noreply.github.com> Date: Sun, 23 Mar 2025 13:12:41 +0000 Subject: [PATCH] telemetry module (#934) * update lockfile * fix color check * telemetry * complete * Use interval --- apps/server/package.json | 1 + apps/server/src/app.module.ts | 2 + .../environment/environment.service.ts | 7 ++ .../telemetry/telemetry.module.ts | 9 ++ .../telemetry/telemetry.service.ts | 87 +++++++++++++++++++ pnpm-lock.yaml | 34 ++++++++ 6 files changed, 140 insertions(+) create mode 100644 apps/server/src/integrations/telemetry/telemetry.module.ts create mode 100644 apps/server/src/integrations/telemetry/telemetry.service.ts diff --git a/apps/server/package.json b/apps/server/package.json index 6db80240..f9742c5a 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -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", diff --git a/apps/server/src/app.module.ts b/apps/server/src/app.module.ts index 5aa30e60..052faed2 100644 --- a/apps/server/src/app.module.ts +++ b/apps/server/src/app.module.ts @@ -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], diff --git a/apps/server/src/integrations/environment/environment.service.ts b/apps/server/src/integrations/environment/environment.service.ts index 6854184e..ac26b4fb 100644 --- a/apps/server/src/integrations/environment/environment.service.ts +++ b/apps/server/src/integrations/environment/environment.service.ts @@ -182,4 +182,11 @@ export class EnvironmentService { .toLowerCase(); return isStandalone === 'true'; } + + isDisableTelemetry(): boolean { + const disable = this.configService + .get('DISABLE_TELEMETRY', 'false') + .toLowerCase(); + return disable === 'true'; + } } diff --git a/apps/server/src/integrations/telemetry/telemetry.module.ts b/apps/server/src/integrations/telemetry/telemetry.module.ts new file mode 100644 index 00000000..796afec9 --- /dev/null +++ b/apps/server/src/integrations/telemetry/telemetry.module.ts @@ -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 {} diff --git a/apps/server/src/integrations/telemetry/telemetry.service.ts b/apps/server/src/integrations/telemetry/telemetry.service.ts new file mode 100644 index 00000000..99473e9d --- /dev/null +++ b/apps/server/src/integrations/telemetry/telemetry.service.ts @@ -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 */ + } + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d81c0246..0a053674 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -444,6 +444,9 @@ importers: '@nestjs/platform-socket.io': specifier: ^11.0.10 version: 11.0.10(@nestjs/common@11.0.10(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/websockets@11.0.10)(rxjs@7.8.1) + '@nestjs/schedule': + specifier: ^5.0.1 + version: 5.0.1(@nestjs/common@11.0.10(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@11.0.10) '@nestjs/terminus': specifier: ^11.0.0 version: 11.0.0(@nestjs/common@11.0.10(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@11.0.10)(reflect-metadata@0.2.2)(rxjs@7.8.1) @@ -2820,6 +2823,12 @@ packages: '@nestjs/websockets': ^11.0.0 rxjs: ^7.1.0 + '@nestjs/schedule@5.0.1': + resolution: {integrity: sha512-kFoel84I4RyS2LNPH6yHYTKxB16tb3auAEciFuc788C3ph6nABkUfzX5IE+unjVaRX+3GuruJwurNepMlHXpQg==} + peerDependencies: + '@nestjs/common': ^10.0.0 || ^11.0.0 + '@nestjs/core': ^10.0.0 || ^11.0.0 + '@nestjs/schematics@11.0.1': resolution: {integrity: sha512-PHPAUk4sXkfCxiMacD1JFC+vEyzXjZJRCu1KT2MmG2hrTiMDMk5KtMprro148JUefNuWbVyN0uLTJVSmWVzhoA==} peerDependencies: @@ -4119,6 +4128,9 @@ packages: '@types/linkify-it@5.0.0': resolution: {integrity: sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==} + '@types/luxon@3.4.2': + resolution: {integrity: sha512-TifLZlFudklWlMBfhubvgqTXRzLDI5pCbGa4P8a3wPyUQSW+1xQ5eDsreP9DWHX3tjq1ke96uYG/nwundroWcA==} + '@types/markdown-it@14.1.2': resolution: {integrity: sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==} @@ -5060,6 +5072,9 @@ packages: resolution: {integrity: sha512-p0SaNjrHOnQeR8/VnfGbmg9te2kfyYSQ7Sc/j/6DtPL3JQvKxmjO9TSjNFpujqV3vEYYBvNNvXSxzyksBWAx1Q==} engines: {node: '>=12.0.0'} + cron@3.5.0: + resolution: {integrity: sha512-0eYZqCnapmxYcV06uktql93wNWdlTmmBFP2iYz+JPVcQqlyFYcn1lFuIk4R54pkOmE7mcldTAPZv6X5XA4Q46A==} + cross-env@7.0.3: resolution: {integrity: sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==} engines: {node: '>=10.14', npm: '>=6', yarn: '>=1'} @@ -6822,6 +6837,10 @@ packages: resolution: {integrity: sha512-zobTr7akeGHnv7eBOXcRgMeCP6+uyYsczwmeRCauvpvaAltgNyTbLH/+VaEAPUeWBT+1GuNmz4wC/6jtQzbbVA==} engines: {node: '>=12'} + luxon@3.5.0: + resolution: {integrity: sha512-rh+Zjr6DNfUYR3bPwJEnuwDdqMbxZW7LOQfUN4B54+Cl+0o5zaU9RJ6bcidfDtC1cWCZXQ+nvX8bf6bAji37QQ==} + engines: {node: '>=12'} + magic-string@0.30.17: resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==} @@ -11954,6 +11973,12 @@ snapshots: - supports-color - utf-8-validate + '@nestjs/schedule@5.0.1(@nestjs/common@11.0.10(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@11.0.10)': + dependencies: + '@nestjs/common': 11.0.10(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1) + '@nestjs/core': 11.0.10(@nestjs/common@11.0.10(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/websockets@11.0.10)(reflect-metadata@0.2.2)(rxjs@7.8.1) + cron: 3.5.0 + '@nestjs/schematics@11.0.1(chokidar@4.0.3)(typescript@5.7.3)': dependencies: '@angular-devkit/core': 19.1.7(chokidar@4.0.3) @@ -13331,6 +13356,8 @@ snapshots: '@types/linkify-it@5.0.0': {} + '@types/luxon@3.4.2': {} + '@types/markdown-it@14.1.2': dependencies: '@types/linkify-it': 5.0.0 @@ -14486,6 +14513,11 @@ snapshots: dependencies: luxon: 3.4.4 + cron@3.5.0: + dependencies: + '@types/luxon': 3.4.2 + luxon: 3.5.0 + cross-env@7.0.3: dependencies: cross-spawn: 7.0.3 @@ -16688,6 +16720,8 @@ snapshots: luxon@3.4.4: {} + luxon@3.5.0: {} + magic-string@0.30.17: dependencies: '@jridgewell/sourcemap-codec': 1.5.0