diff --git a/packages/cli/src/InternalHooks.ts b/packages/cli/src/InternalHooks.ts index 7c923388ed81c..e6418743d7b1a 100644 --- a/packages/cli/src/InternalHooks.ts +++ b/packages/cli/src/InternalHooks.ts @@ -1,8 +1,6 @@ import { Service } from 'typedi'; import { snakeCase } from 'change-case'; -import os from 'node:os'; import { get as pslGet } from 'psl'; -import { GlobalConfig } from '@n8n/config'; import type { ExecutionStatus, INodesGraphResult, @@ -11,27 +9,23 @@ import type { IWorkflowBase, } from 'n8n-workflow'; import { TelemetryHelpers } from 'n8n-workflow'; -import { InstanceSettings } from 'n8n-core'; import config from '@/config'; import { N8N_VERSION } from '@/constants'; import type { AuthProviderType } from '@db/entities/AuthIdentity'; import type { User } from '@db/entities/User'; import { SharedWorkflowRepository } from '@db/repositories/sharedWorkflow.repository'; -import { WorkflowRepository } from '@db/repositories/workflow.repository'; import { determineFinalExecutionStatus } from '@/executionLifecycleHooks/shared/sharedHookFunctions'; import type { ITelemetryUserDeletionData, IWorkflowDb, IExecutionTrackProperties, } from '@/Interfaces'; -import { License } from '@/License'; import { WorkflowStatisticsService } from '@/services/workflow-statistics.service'; import { NodeTypes } from '@/NodeTypes'; import { Telemetry } from '@/telemetry'; import type { Project } from '@db/entities/Project'; import { ProjectRelationRepository } from './databases/repositories/projectRelation.repository'; -import { SharedCredentialsRepository } from './databases/repositories/sharedCredentials.repository'; import { MessageEventBus } from './eventbus/MessageEventBus/MessageEventBus'; /** @@ -42,16 +36,11 @@ import { MessageEventBus } from './eventbus/MessageEventBus/MessageEventBus'; @Service() export class InternalHooks { constructor( - private readonly globalConfig: GlobalConfig, private readonly telemetry: Telemetry, private readonly nodeTypes: NodeTypes, private readonly sharedWorkflowRepository: SharedWorkflowRepository, - private readonly workflowRepository: WorkflowRepository, workflowStatisticsService: WorkflowStatisticsService, - private readonly instanceSettings: InstanceSettings, - private readonly license: License, private readonly projectRelationRepository: ProjectRelationRepository, - private readonly sharedCredentialsRepository: SharedCredentialsRepository, private readonly _eventBus: MessageEventBus, // needed until we decouple telemetry ) { workflowStatisticsService.on( @@ -68,73 +57,6 @@ export class InternalHooks { await this.telemetry.init(); } - async onServerStarted(): Promise { - const cpus = os.cpus(); - const binaryDataConfig = config.getEnv('binaryDataManager'); - - const isS3Selected = config.getEnv('binaryDataManager.mode') === 's3'; - const isS3Available = config.getEnv('binaryDataManager.availableModes').includes('s3'); - const isS3Licensed = this.license.isBinaryDataS3Licensed(); - const authenticationMethod = config.getEnv('userManagement.authenticationMethod'); - - const info = { - version_cli: N8N_VERSION, - db_type: this.globalConfig.database.type, - n8n_version_notifications_enabled: this.globalConfig.versionNotifications.enabled, - n8n_disable_production_main_process: config.getEnv( - 'endpoints.disableProductionWebhooksOnMainProcess', - ), - system_info: { - os: { - type: os.type(), - version: os.version(), - }, - memory: os.totalmem() / 1024, - cpus: { - count: cpus.length, - model: cpus[0].model, - speed: cpus[0].speed, - }, - }, - execution_variables: { - executions_mode: config.getEnv('executions.mode'), - executions_timeout: config.getEnv('executions.timeout'), - executions_timeout_max: config.getEnv('executions.maxTimeout'), - executions_data_save_on_error: config.getEnv('executions.saveDataOnError'), - executions_data_save_on_success: config.getEnv('executions.saveDataOnSuccess'), - executions_data_save_on_progress: config.getEnv('executions.saveExecutionProgress'), - executions_data_save_manual_executions: config.getEnv( - 'executions.saveDataManualExecutions', - ), - executions_data_prune: config.getEnv('executions.pruneData'), - executions_data_max_age: config.getEnv('executions.pruneDataMaxAge'), - }, - n8n_deployment_type: config.getEnv('deployment.type'), - n8n_binary_data_mode: binaryDataConfig.mode, - smtp_set_up: this.globalConfig.userManagement.emails.mode === 'smtp', - ldap_allowed: authenticationMethod === 'ldap', - saml_enabled: authenticationMethod === 'saml', - license_plan_name: this.license.getPlanName(), - license_tenant_id: config.getEnv('license.tenantId'), - binary_data_s3: isS3Available && isS3Selected && isS3Licensed, - multi_main_setup_enabled: config.getEnv('multiMainSetup.enabled'), - }; - - const firstWorkflow = await this.workflowRepository.findOne({ - select: ['createdAt'], - order: { createdAt: 'ASC' }, - where: {}, - }); - - return await Promise.all([ - this.telemetry.identify(info), - this.telemetry.track('Instance started', { - ...info, - earliest_workflow_created: firstWorkflow?.createdAt, - }), - ]); - } - async onFrontendSettingsAPI(pushRef?: string): Promise { return await this.telemetry.track('Session started', { session_id: pushRef }); } diff --git a/packages/cli/src/Server.ts b/packages/cli/src/Server.ts index a31a87f91d2a2..dc63d697e77b8 100644 --- a/packages/cli/src/Server.ts +++ b/packages/cli/src/Server.ts @@ -33,7 +33,6 @@ import { isLdapEnabled } from '@/Ldap/helpers.ee'; import { AbstractServer } from '@/AbstractServer'; import { PostHogClient } from '@/posthog'; import { MessageEventBus } from '@/eventbus/MessageEventBus/MessageEventBus'; -import { InternalHooks } from '@/InternalHooks'; import { handleMfaDisable, isMfaFeatureEnabled } from '@/Mfa/helpers'; import type { FrontendService } from '@/services/frontend.service'; import { OrchestrationService } from '@/services/orchestration.service'; @@ -66,6 +65,7 @@ import '@/ExternalSecrets/ExternalSecrets.controller.ee'; import '@/license/license.controller'; import '@/workflows/workflowHistory/workflowHistory.controller.ee'; import '@/workflows/workflows.controller'; +import { EventService } from './eventbus/event.service'; const exec = promisify(callbackExec); @@ -82,6 +82,7 @@ export class Server extends AbstractServer { private readonly orchestrationService: OrchestrationService, private readonly postHogClient: PostHogClient, private readonly globalConfig: GlobalConfig, + private readonly eventService: EventService, ) { super('main'); @@ -106,7 +107,7 @@ export class Server extends AbstractServer { void this.loadNodesAndCredentials.setupHotReload(); } - void Container.get(InternalHooks).onServerStarted(); + this.eventService.emit('server-started'); } private async registerAdditionalControllers() { diff --git a/packages/cli/src/eventbus/event.types.ts b/packages/cli/src/eventbus/event.types.ts index 08f40145a4477..225f9aca8c873 100644 --- a/packages/cli/src/eventbus/event.types.ts +++ b/packages/cli/src/eventbus/event.types.ts @@ -15,6 +15,8 @@ export type UserLike = { * Events sent by `EventService` and forwarded by relays, e.g. `AuditEventRelay` and `TelemetryEventRelay`. */ export type Event = { + 'server-started': {}; + 'workflow-created': { user: UserLike; workflow: IWorkflowBase; diff --git a/packages/cli/src/telemetry/telemetry-event-relay.service.ts b/packages/cli/src/telemetry/telemetry-event-relay.service.ts index ad99243b6aa29..63c7fe4d11e74 100644 --- a/packages/cli/src/telemetry/telemetry-event-relay.service.ts +++ b/packages/cli/src/telemetry/telemetry-event-relay.service.ts @@ -3,12 +3,20 @@ import { EventService } from '@/eventbus/event.service'; import type { Event } from '@/eventbus/event.types'; import { Telemetry } from '.'; import config from '@/config'; +import os from 'node:os'; +import { License } from '@/License'; +import { GlobalConfig } from '@n8n/config'; +import { N8N_VERSION } from '@/constants'; +import { WorkflowRepository } from '@/databases/repositories/workflow.repository'; @Service() export class TelemetryEventRelay { constructor( private readonly eventService: EventService, private readonly telemetry: Telemetry, + private readonly license: License, + private readonly globalConfig: GlobalConfig, + private readonly workflowRepository: WorkflowRepository, ) {} async init() { @@ -20,6 +28,8 @@ export class TelemetryEventRelay { } private setupHandlers() { + this.eventService.on('server-started', async () => await this.serverStarted()); + this.eventService.on('team-project-updated', (event) => this.teamProjectUpdated(event)); this.eventService.on('team-project-deleted', (event) => this.teamProjectDeleted(event)); this.eventService.on('team-project-created', (event) => this.teamProjectCreated(event)); @@ -420,4 +430,71 @@ export class TelemetryEventRelay { private loginFailedDueToLdapDisabled({ userId }: Event['login-failed-due-to-ldap-disabled']) { void this.telemetry.track('User login failed since ldap disabled', { user_ud: userId }); } + + private async serverStarted() { + const cpus = os.cpus(); + const binaryDataConfig = config.getEnv('binaryDataManager'); + + const isS3Selected = config.getEnv('binaryDataManager.mode') === 's3'; + const isS3Available = config.getEnv('binaryDataManager.availableModes').includes('s3'); + const isS3Licensed = this.license.isBinaryDataS3Licensed(); + const authenticationMethod = config.getEnv('userManagement.authenticationMethod'); + + const info = { + version_cli: N8N_VERSION, + db_type: this.globalConfig.database.type, + n8n_version_notifications_enabled: this.globalConfig.versionNotifications.enabled, + n8n_disable_production_main_process: config.getEnv( + 'endpoints.disableProductionWebhooksOnMainProcess', + ), + system_info: { + os: { + type: os.type(), + version: os.version(), + }, + memory: os.totalmem() / 1024, + cpus: { + count: cpus.length, + model: cpus[0].model, + speed: cpus[0].speed, + }, + }, + execution_variables: { + executions_mode: config.getEnv('executions.mode'), + executions_timeout: config.getEnv('executions.timeout'), + executions_timeout_max: config.getEnv('executions.maxTimeout'), + executions_data_save_on_error: config.getEnv('executions.saveDataOnError'), + executions_data_save_on_success: config.getEnv('executions.saveDataOnSuccess'), + executions_data_save_on_progress: config.getEnv('executions.saveExecutionProgress'), + executions_data_save_manual_executions: config.getEnv( + 'executions.saveDataManualExecutions', + ), + executions_data_prune: config.getEnv('executions.pruneData'), + executions_data_max_age: config.getEnv('executions.pruneDataMaxAge'), + }, + n8n_deployment_type: config.getEnv('deployment.type'), + n8n_binary_data_mode: binaryDataConfig.mode, + smtp_set_up: this.globalConfig.userManagement.emails.mode === 'smtp', + ldap_allowed: authenticationMethod === 'ldap', + saml_enabled: authenticationMethod === 'saml', + license_plan_name: this.license.getPlanName(), + license_tenant_id: config.getEnv('license.tenantId'), + binary_data_s3: isS3Available && isS3Selected && isS3Licensed, + multi_main_setup_enabled: config.getEnv('multiMainSetup.enabled'), + }; + + const firstWorkflow = await this.workflowRepository.findOne({ + select: ['createdAt'], + order: { createdAt: 'ASC' }, + where: {}, + }); + + void Promise.all([ + this.telemetry.identify(info), + this.telemetry.track('Instance started', { + ...info, + earliest_workflow_created: firstWorkflow?.createdAt, + }), + ]); + } } diff --git a/packages/cli/test/unit/InternalHooks.test.ts b/packages/cli/test/unit/InternalHooks.test.ts deleted file mode 100644 index 09835e4813ba5..0000000000000 --- a/packages/cli/test/unit/InternalHooks.test.ts +++ /dev/null @@ -1,97 +0,0 @@ -import { mock } from 'jest-mock-extended'; -import type { GlobalConfig } from '@n8n/config'; -import { N8N_VERSION } from '@/constants'; -import { InternalHooks } from '@/InternalHooks'; -import type { License } from '@/License'; -import type { Telemetry } from '@/telemetry'; - -jest.mock('node:os', () => ({ - tmpdir: () => '', - cpus: () => [{ model: 'MIPS R3000', speed: 40_000_000 }], - type: () => 'TempleOS', - version: () => '5.03', - totalmem: () => 1024 * 1024, -})); - -describe('InternalHooks', () => { - const telemetry = mock(); - const license = mock(); - const globalConfig = mock({ - database: { - type: 'sqlite', - }, - userManagement: { - emails: { - mode: 'smtp', - }, - }, - versionNotifications: { - enabled: true, - }, - }); - const internalHooks = new InternalHooks( - globalConfig, - telemetry, - mock(), - mock(), - mock(), - mock(), - mock(), - license, - mock(), - mock(), - mock(), - ); - - beforeEach(() => jest.clearAllMocks()); - - it('Should be defined', () => { - expect(internalHooks).toBeDefined(); - }); - - it('Should forward license plan name and tenant id to identify when provided', async () => { - license.getPlanName.mockReturnValue('Best Plan'); - globalConfig.database.type = 'sqlite'; - - await internalHooks.onServerStarted(); - - expect(telemetry.identify).toHaveBeenCalledWith({ - version_cli: N8N_VERSION, - db_type: 'sqlite', - n8n_version_notifications_enabled: true, - n8n_disable_production_main_process: false, - system_info: { - memory: 1024, - os: { - type: 'TempleOS', - version: '5.03', - }, - cpus: { - count: 1, - model: 'MIPS R3000', - speed: 40000000, - }, - }, - execution_variables: { - executions_data_max_age: 336, - executions_data_prune: true, - executions_data_save_manual_executions: true, - executions_data_save_on_error: 'all', - executions_data_save_on_progress: false, - executions_data_save_on_success: 'all', - executions_mode: 'regular', - executions_timeout: -1, - executions_timeout_max: 3600, - }, - n8n_deployment_type: 'default', - n8n_binary_data_mode: 'default', - smtp_set_up: true, - ldap_allowed: false, - saml_enabled: false, - license_plan_name: 'Best Plan', - license_tenant_id: 1, - binary_data_s3: false, - multi_main_setup_enabled: false, - }); - }); -});