diff --git a/packages/cli/src/InternalHooks.ts b/packages/cli/src/InternalHooks.ts index 50067bc828..c621ef829d 100644 --- a/packages/cli/src/InternalHooks.ts +++ b/packages/cli/src/InternalHooks.ts @@ -16,7 +16,7 @@ import { InstanceSettings } from 'n8n-core'; import config from '@/config'; import { N8N_VERSION } from '@/constants'; import type { AuthProviderType } from '@db/entities/AuthIdentity'; -import type { GlobalRole, User } from '@db/entities/User'; +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'; @@ -30,7 +30,6 @@ import { EventsService } from '@/services/events.service'; import { NodeTypes } from '@/NodeTypes'; import { Telemetry } from '@/telemetry'; import type { Project } from '@db/entities/Project'; -import type { ProjectRole } from '@db/entities/ProjectRelation'; import { ProjectRelationRepository } from './databases/repositories/projectRelation.repository'; import { SharedCredentialsRepository } from './databases/repositories/sharedCredentials.repository'; import { MessageEventBus } from './eventbus/MessageEventBus/MessageEventBus'; @@ -834,31 +833,4 @@ export class InternalHooks { }): Promise { return await this.telemetry.track('User updated external secrets settings', saveData); } - - async onTeamProjectCreated(data: { user_id: string; role: GlobalRole }) { - return await this.telemetry.track('User created project', data); - } - - async onTeamProjectDeleted(data: { - user_id: string; - role: GlobalRole; - project_id: string; - removal_type: 'delete' | 'transfer'; - target_project_id?: string; - }) { - return await this.telemetry.track('User deleted project', data); - } - - async onTeamProjectUpdated(data: { - user_id: string; - role: GlobalRole; - project_id: string; - members: Array<{ user_id: string; role: ProjectRole }>; - }) { - return await this.telemetry.track('Project settings updated', data); - } - - async onConcurrencyLimitHit({ threshold }: { threshold: number }) { - await this.telemetry.track('User hit concurrency limit', { threshold }); - } } diff --git a/packages/cli/src/commands/BaseCommand.ts b/packages/cli/src/commands/BaseCommand.ts index 130aedf961..a1fa342413 100644 --- a/packages/cli/src/commands/BaseCommand.ts +++ b/packages/cli/src/commands/BaseCommand.ts @@ -23,6 +23,7 @@ import { initExpressionEvaluator } from '@/ExpressionEvaluator'; import { generateHostInstanceId } from '@db/utils/generators'; import { WorkflowHistoryManager } from '@/workflows/workflowHistory/workflowHistoryManager.ee'; import { ShutdownService } from '@/shutdown/Shutdown.service'; +import { TelemetryEventRelay } from '@/telemetry/telemetry-event-relay.service'; export abstract class BaseCommand extends Command { protected logger = Container.get(Logger); @@ -111,6 +112,7 @@ export abstract class BaseCommand extends Command { await Container.get(PostHogClient).init(); await Container.get(InternalHooks).init(); + await Container.get(TelemetryEventRelay).init(); } protected setInstanceType(instanceType: N8nInstanceType) { diff --git a/packages/cli/src/controllers/project.controller.ts b/packages/cli/src/controllers/project.controller.ts index 3b5871f13c..2b9c7dc003 100644 --- a/packages/cli/src/controllers/project.controller.ts +++ b/packages/cli/src/controllers/project.controller.ts @@ -23,7 +23,7 @@ import { ProjectRepository } from '@/databases/repositories/project.repository'; // eslint-disable-next-line n8n-local-rules/misplaced-n8n-typeorm-import import { In, Not } from '@n8n/typeorm'; import { BadRequestError } from '@/errors/response-errors/bad-request.error'; -import { InternalHooks } from '@/InternalHooks'; +import { EventRelay } from '@/eventbus/event-relay.service'; @RestController('/projects') export class ProjectController { @@ -31,7 +31,7 @@ export class ProjectController { private readonly projectsService: ProjectService, private readonly roleService: RoleService, private readonly projectRepository: ProjectRepository, - private readonly internalHooks: InternalHooks, + private readonly eventRelay: EventRelay, ) {} @Get('/') @@ -52,8 +52,8 @@ export class ProjectController { try { const project = await this.projectsService.createTeamProject(req.body.name, req.user); - void this.internalHooks.onTeamProjectCreated({ - user_id: req.user.id, + this.eventRelay.emit('team-project-created', { + userId: req.user.id, role: req.user.role, }); @@ -195,11 +195,11 @@ export class ProjectController { throw e; } - void this.internalHooks.onTeamProjectUpdated({ - user_id: req.user.id, + this.eventRelay.emit('team-project-updated', { + userId: req.user.id, role: req.user.role, - members: req.body.relations.map(({ userId, role }) => ({ user_id: userId, role })), - project_id: req.params.projectId, + members: req.body.relations, + projectId: req.params.projectId, }); } } @@ -211,12 +211,12 @@ export class ProjectController { migrateToProject: req.query.transferId, }); - void this.internalHooks.onTeamProjectDeleted({ - user_id: req.user.id, + this.eventRelay.emit('team-project-deleted', { + userId: req.user.id, role: req.user.role, - project_id: req.params.projectId, - removal_type: req.query.transferId !== undefined ? 'transfer' : 'delete', - target_project_id: req.query.transferId, + projectId: req.params.projectId, + removalType: req.query.transferId !== undefined ? 'transfer' : 'delete', + targetProjectId: req.query.transferId, }); } } diff --git a/packages/cli/src/eventbus/event.types.ts b/packages/cli/src/eventbus/event.types.ts index 12bfa12ae8..ef1d04a090 100644 --- a/packages/cli/src/eventbus/event.types.ts +++ b/packages/cli/src/eventbus/event.types.ts @@ -1,5 +1,7 @@ import type { AuthenticationMethod, IWorkflowBase } from 'n8n-workflow'; import type { IWorkflowExecutionDataProcess } from '@/Interfaces'; +import type { ProjectRole } from '@/databases/entities/ProjectRelation'; +import type { GlobalRole } from '@/databases/entities/User'; export type UserLike = { id: string; @@ -10,7 +12,7 @@ export type UserLike = { }; /** - * Events sent by services and consumed by relays, e.g. `AuditEventRelay`. + * Events sent by services and consumed by relays, e.g. `AuditEventRelay` and `TelemetryEventRelay`. */ export type Event = { 'workflow-created': { @@ -190,4 +192,27 @@ export type Event = { 'execution-started-during-bootup': { executionId: string; }; + + 'team-project-updated': { + userId: string; + role: GlobalRole; + members: Array<{ + userId: string; + role: ProjectRole; + }>; + projectId: string; + }; + + 'team-project-deleted': { + userId: string; + role: GlobalRole; + projectId: string; + removalType: 'transfer' | 'delete'; + targetProjectId?: string; + }; + + 'team-project-created': { + userId: string; + role: GlobalRole; + }; }; diff --git a/packages/cli/src/telemetry/telemetry-event-relay.service.ts b/packages/cli/src/telemetry/telemetry-event-relay.service.ts new file mode 100644 index 0000000000..1125baa84d --- /dev/null +++ b/packages/cli/src/telemetry/telemetry-event-relay.service.ts @@ -0,0 +1,60 @@ +import { Service } from 'typedi'; +import { EventRelay } from '@/eventbus/event-relay.service'; +import type { Event } from '@/eventbus/event.types'; +import { Telemetry } from '.'; +import config from '@/config'; + +@Service() +export class TelemetryEventRelay { + constructor( + private readonly eventRelay: EventRelay, + private readonly telemetry: Telemetry, + ) {} + + async init() { + if (!config.getEnv('diagnostics.enabled')) return; + + await this.telemetry.init(); + + this.setupHandlers(); + } + + private setupHandlers() { + this.eventRelay.on('team-project-updated', (event) => this.teamProjectUpdated(event)); + this.eventRelay.on('team-project-deleted', (event) => this.teamProjectDeleted(event)); + this.eventRelay.on('team-project-created', (event) => this.teamProjectCreated(event)); + } + + private teamProjectUpdated({ userId, role, members, projectId }: Event['team-project-updated']) { + void this.telemetry.track('Project settings updated', { + user_id: userId, + role, + // eslint-disable-next-line @typescript-eslint/no-shadow + members: members.map(({ userId: user_id, role }) => ({ user_id, role })), + project_id: projectId, + }); + } + + private teamProjectDeleted({ + userId, + role, + projectId, + removalType, + targetProjectId, + }: Event['team-project-deleted']) { + void this.telemetry.track('User deleted project', { + user_id: userId, + role, + project_id: projectId, + removal_type: removalType, + target_project_id: targetProjectId, + }); + } + + private teamProjectCreated({ userId, role }: Event['team-project-created']) { + void this.telemetry.track('User created project', { + user_id: userId, + role, + }); + } +} diff --git a/packages/cli/test/integration/shared/utils/testCommand.ts b/packages/cli/test/integration/shared/utils/testCommand.ts index ff36791ad5..7a8477c4dd 100644 --- a/packages/cli/test/integration/shared/utils/testCommand.ts +++ b/packages/cli/test/integration/shared/utils/testCommand.ts @@ -4,6 +4,8 @@ import { mock } from 'jest-mock-extended'; import type { BaseCommand } from '@/commands/BaseCommand'; import * as testDb from '../testDb'; +import { TelemetryEventRelay } from '@/telemetry/telemetry-event-relay.service'; +import { mockInstance } from '@test/mocking'; export const setupTestCommand = (Command: Class) => { const config = mock(); @@ -19,6 +21,7 @@ export const setupTestCommand = (Command: Class) => { beforeEach(() => { jest.clearAllMocks(); + mockInstance(TelemetryEventRelay); }); afterAll(async () => {