diff --git a/packages/cli/src/InternalHooks.ts b/packages/cli/src/InternalHooks.ts index 689a8a7303..8647ecf978 100644 --- a/packages/cli/src/InternalHooks.ts +++ b/packages/cli/src/InternalHooks.ts @@ -775,55 +775,6 @@ export class InternalHooks { return await this.telemetry.track('User created variable', createData); } - async onSourceControlSettingsUpdated(data: { - branch_name: string; - read_only_instance: boolean; - repo_type: 'github' | 'gitlab' | 'other'; - connected: boolean; - }): Promise { - return await this.telemetry.track('User updated source control settings', data); - } - - async onSourceControlUserStartedPullUI(data: { - workflow_updates: number; - workflow_conflicts: number; - cred_conflicts: number; - }): Promise { - return await this.telemetry.track('User started pull via UI', data); - } - - async onSourceControlUserFinishedPullUI(data: { workflow_updates: number }): Promise { - return await this.telemetry.track('User finished pull via UI', { - workflow_updates: data.workflow_updates, - }); - } - - async onSourceControlUserPulledAPI(data: { - workflow_updates: number; - forced: boolean; - }): Promise { - return await this.telemetry.track('User pulled via API', data); - } - - async onSourceControlUserStartedPushUI(data: { - workflows_eligible: number; - workflows_eligible_with_conflicts: number; - creds_eligible: number; - creds_eligible_with_conflicts: number; - variables_eligible: number; - }): Promise { - return await this.telemetry.track('User started push via UI', data); - } - - async onSourceControlUserFinishedPushUI(data: { - workflows_eligible: number; - workflows_pushed: number; - creds_pushed: number; - variables_pushed: number; - }): Promise { - return await this.telemetry.track('User finished push via UI', data); - } - async onExternalSecretsProviderSettingsSaved(saveData: { user_id?: string | undefined; vault_type: string; diff --git a/packages/cli/src/PublicApi/v1/handlers/sourceControl/sourceControl.handler.ts b/packages/cli/src/PublicApi/v1/handlers/sourceControl/sourceControl.handler.ts index a413290c56..c1113a5e70 100644 --- a/packages/cli/src/PublicApi/v1/handlers/sourceControl/sourceControl.handler.ts +++ b/packages/cli/src/PublicApi/v1/handlers/sourceControl/sourceControl.handler.ts @@ -10,7 +10,7 @@ import { getTrackingInformationFromPullResult, isSourceControlLicensed, } from '@/environments/sourceControl/sourceControlHelper.ee'; -import { InternalHooks } from '@/InternalHooks'; +import { EventRelay } from '@/eventbus/event-relay.service'; export = { pull: [ @@ -39,7 +39,7 @@ export = { }); if (result.statusCode === 200) { - void Container.get(InternalHooks).onSourceControlUserPulledAPI({ + Container.get(EventRelay).emit('source-control-user-pulled-api', { ...getTrackingInformationFromPullResult(result.statusResult), forced: req.body.force ?? false, }); diff --git a/packages/cli/src/environments/sourceControl/sourceControl.controller.ee.ts b/packages/cli/src/environments/sourceControl/sourceControl.controller.ee.ts index 77d7330799..e7e32d3547 100644 --- a/packages/cli/src/environments/sourceControl/sourceControl.controller.ee.ts +++ b/packages/cli/src/environments/sourceControl/sourceControl.controller.ee.ts @@ -12,7 +12,7 @@ import type { SourceControlPreferences } from './types/sourceControlPreferences' import type { SourceControlledFile } from './types/sourceControlledFile'; import { SOURCE_CONTROL_DEFAULT_BRANCH } from './constants'; import type { ImportResult } from './types/importResult'; -import { InternalHooks } from '@/InternalHooks'; +import { EventRelay } from '@/eventbus/event-relay.service'; import { getRepoType } from './sourceControlHelper.ee'; import { SourceControlGetStatus } from './types/sourceControlGetStatus'; import { BadRequestError } from '@/errors/response-errors/bad-request.error'; @@ -22,7 +22,7 @@ export class SourceControlController { constructor( private readonly sourceControlService: SourceControlService, private readonly sourceControlPreferencesService: SourceControlPreferencesService, - private readonly internalHooks: InternalHooks, + private readonly eventRelay: EventRelay, ) {} @Get('/preferences', { middlewares: [sourceControlLicensedMiddleware], skipAuth: true }) @@ -83,11 +83,11 @@ export class SourceControlController { const resultingPreferences = this.sourceControlPreferencesService.getPreferences(); // #region Tracking Information // located in controller so as to not call this multiple times when updating preferences - void this.internalHooks.onSourceControlSettingsUpdated({ - branch_name: resultingPreferences.branchName, + this.eventRelay.emit('source-control-settings-updated', { + branchName: resultingPreferences.branchName, connected: resultingPreferences.connected, - read_only_instance: resultingPreferences.branchReadOnly, - repo_type: getRepoType(resultingPreferences.repositoryUrl), + readOnlyInstance: resultingPreferences.branchReadOnly, + repoType: getRepoType(resultingPreferences.repositoryUrl), }); // #endregion return resultingPreferences; @@ -128,11 +128,11 @@ export class SourceControlController { } await this.sourceControlService.init(); const resultingPreferences = this.sourceControlPreferencesService.getPreferences(); - void this.internalHooks.onSourceControlSettingsUpdated({ - branch_name: resultingPreferences.branchName, + this.eventRelay.emit('source-control-settings-updated', { + branchName: resultingPreferences.branchName, connected: resultingPreferences.connected, - read_only_instance: resultingPreferences.branchReadOnly, - repo_type: getRepoType(resultingPreferences.repositoryUrl), + readOnlyInstance: resultingPreferences.branchReadOnly, + repoType: getRepoType(resultingPreferences.repositoryUrl), }); return resultingPreferences; } catch (error) { diff --git a/packages/cli/src/environments/sourceControl/sourceControl.service.ee.ts b/packages/cli/src/environments/sourceControl/sourceControl.service.ee.ts index c52c0300d5..8a0d06ad05 100644 --- a/packages/cli/src/environments/sourceControl/sourceControl.service.ee.ts +++ b/packages/cli/src/environments/sourceControl/sourceControl.service.ee.ts @@ -1,4 +1,4 @@ -import Container, { Service } from 'typedi'; +import { Service } from 'typedi'; import path from 'path'; import { getTagsPath, @@ -30,7 +30,7 @@ import type { TagEntity } from '@db/entities/TagEntity'; import type { Variables } from '@db/entities/Variables'; import type { SourceControlWorkflowVersionId } from './types/sourceControlWorkflowVersionId'; import type { ExportableCredential } from './types/exportableCredential'; -import { InternalHooks } from '@/InternalHooks'; +import { EventRelay } from '@/eventbus/event-relay.service'; import { TagRepository } from '@db/repositories/tag.repository'; import { Logger } from '@/Logger'; import { BadRequestError } from '@/errors/response-errors/bad-request.error'; @@ -52,6 +52,7 @@ export class SourceControlService { private sourceControlExportService: SourceControlExportService, private sourceControlImportService: SourceControlImportService, private tagRepository: TagRepository, + private readonly eventRelay: EventRelay, ) { const { gitFolder, sshFolder, sshKeyName } = sourceControlPreferencesService; this.gitFolder = gitFolder; @@ -291,7 +292,8 @@ export class SourceControlService { }); // #region Tracking Information - void Container.get(InternalHooks).onSourceControlUserFinishedPushUI( + this.eventRelay.emit( + 'source-control-user-finished-push-ui', getTrackingInformationFromPostPushResult(statusResult), ); // #endregion @@ -368,7 +370,8 @@ export class SourceControlService { } // #region Tracking Information - void Container.get(InternalHooks).onSourceControlUserFinishedPullUI( + this.eventRelay.emit( + 'source-control-user-finished-pull-ui', getTrackingInformationFromPullResult(statusResult), ); // #endregion @@ -421,11 +424,13 @@ export class SourceControlService { // #region Tracking Information if (options.direction === 'push') { - void Container.get(InternalHooks).onSourceControlUserStartedPushUI( + this.eventRelay.emit( + 'source-control-user-started-push-ui', getTrackingInformationFromPrePushResult(sourceControlledFiles), ); } else if (options.direction === 'pull') { - void Container.get(InternalHooks).onSourceControlUserStartedPullUI( + this.eventRelay.emit( + 'source-control-user-started-pull-ui', getTrackingInformationFromPullResult(sourceControlledFiles), ); } diff --git a/packages/cli/src/environments/sourceControl/sourceControlHelper.ee.ts b/packages/cli/src/environments/sourceControl/sourceControlHelper.ee.ts index 6c09f3fd56..8610f03015 100644 --- a/packages/cli/src/environments/sourceControl/sourceControlHelper.ee.ts +++ b/packages/cli/src/environments/sourceControl/sourceControlHelper.ee.ts @@ -121,58 +121,43 @@ function filterSourceControlledFilesUniqueIds(files: SourceControlledFile[]) { ); } -export function getTrackingInformationFromPullResult(result: SourceControlledFile[]): { - cred_conflicts: number; - workflow_conflicts: number; - workflow_updates: number; -} { +export function getTrackingInformationFromPullResult(result: SourceControlledFile[]) { const uniques = filterSourceControlledFilesUniqueIds(result); return { - cred_conflicts: uniques.filter( + credConflicts: uniques.filter( (file) => file.type === 'credential' && file.status === 'modified' && file.location === 'local', ).length, - workflow_conflicts: uniques.filter( + workflowConflicts: uniques.filter( (file) => file.type === 'workflow' && file.status === 'modified' && file.location === 'local', ).length, - workflow_updates: uniques.filter((file) => file.type === 'workflow').length, + workflowUpdates: uniques.filter((file) => file.type === 'workflow').length, }; } -export function getTrackingInformationFromPrePushResult(result: SourceControlledFile[]): { - workflows_eligible: number; - workflows_eligible_with_conflicts: number; - creds_eligible: number; - creds_eligible_with_conflicts: number; - variables_eligible: number; -} { +export function getTrackingInformationFromPrePushResult(result: SourceControlledFile[]) { const uniques = filterSourceControlledFilesUniqueIds(result); return { - workflows_eligible: uniques.filter((file) => file.type === 'workflow').length, - workflows_eligible_with_conflicts: uniques.filter( + workflowsEligible: uniques.filter((file) => file.type === 'workflow').length, + workflowsEligibleWithConflicts: uniques.filter( (file) => file.type === 'workflow' && file.conflict, ).length, - creds_eligible: uniques.filter((file) => file.type === 'credential').length, - creds_eligible_with_conflicts: uniques.filter( + credsEligible: uniques.filter((file) => file.type === 'credential').length, + credsEligibleWithConflicts: uniques.filter( (file) => file.type === 'credential' && file.conflict, ).length, - variables_eligible: uniques.filter((file) => file.type === 'variables').length, + variablesEligible: uniques.filter((file) => file.type === 'variables').length, }; } -export function getTrackingInformationFromPostPushResult(result: SourceControlledFile[]): { - workflows_eligible: number; - workflows_pushed: number; - creds_pushed: number; - variables_pushed: number; -} { +export function getTrackingInformationFromPostPushResult(result: SourceControlledFile[]) { const uniques = filterSourceControlledFilesUniqueIds(result); return { - workflows_pushed: uniques.filter((file) => file.pushed && file.type === 'workflow').length ?? 0, - workflows_eligible: uniques.filter((file) => file.type === 'workflow').length ?? 0, - creds_pushed: + workflowsPushed: uniques.filter((file) => file.pushed && file.type === 'workflow').length ?? 0, + workflowsEligible: uniques.filter((file) => file.type === 'workflow').length ?? 0, + credsPushed: uniques.filter((file) => file.pushed && file.file.startsWith('credential_stubs')).length ?? 0, - variables_pushed: + variablesPushed: uniques.filter((file) => file.pushed && file.file.startsWith('variable_stubs')).length ?? 0, }; } diff --git a/packages/cli/src/eventbus/event.types.ts b/packages/cli/src/eventbus/event.types.ts index ef1d04a090..a93b4c1204 100644 --- a/packages/cli/src/eventbus/event.types.ts +++ b/packages/cli/src/eventbus/event.types.ts @@ -12,7 +12,7 @@ export type UserLike = { }; /** - * Events sent by services and consumed by relays, e.g. `AuditEventRelay` and `TelemetryEventRelay`. + * Events sent at services and forwarded by relays, e.g. `AuditEventRelay` and `TelemetryEventRelay`. */ export type Event = { 'workflow-created': { @@ -215,4 +215,41 @@ export type Event = { userId: string; role: GlobalRole; }; + + 'source-control-settings-updated': { + branchName: string; + readOnlyInstance: boolean; + repoType: 'github' | 'gitlab' | 'other'; + connected: boolean; + }; + + 'source-control-user-started-pull-ui': { + workflowUpdates: number; + workflowConflicts: number; + credConflicts: number; + }; + + 'source-control-user-finished-pull-ui': { + workflowUpdates: number; + }; + + 'source-control-user-pulled-api': { + workflowUpdates: number; + forced: boolean; + }; + + 'source-control-user-started-push-ui': { + workflowsEligible: number; + workflowsEligibleWithConflicts: number; + credsEligible: number; + credsEligibleWithConflicts: number; + variablesEligible: number; + }; + + 'source-control-user-finished-push-ui': { + workflowsEligible: number; + workflowsPushed: number; + credsPushed: number; + variablesPushed: number; + }; }; diff --git a/packages/cli/src/telemetry/telemetry-event-relay.service.ts b/packages/cli/src/telemetry/telemetry-event-relay.service.ts index 1125baa84d..982376226e 100644 --- a/packages/cli/src/telemetry/telemetry-event-relay.service.ts +++ b/packages/cli/src/telemetry/telemetry-event-relay.service.ts @@ -23,6 +23,24 @@ export class TelemetryEventRelay { 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)); + this.eventRelay.on('source-control-settings-updated', (event) => + this.sourceControlSettingsUpdated(event), + ); + this.eventRelay.on('source-control-user-started-pull-ui', (event) => + this.sourceControlUserStartedPullUi(event), + ); + this.eventRelay.on('source-control-user-finished-pull-ui', (event) => + this.sourceControlUserFinishedPullUi(event), + ); + this.eventRelay.on('source-control-user-pulled-api', (event) => + this.sourceControlUserPulledApi(event), + ); + this.eventRelay.on('source-control-user-started-push-ui', (event) => + this.sourceControlUserStartedPushUi(event), + ); + this.eventRelay.on('source-control-user-finished-push-ui', (event) => + this.sourceControlUserFinishedPushUi(event), + ); } private teamProjectUpdated({ userId, role, members, projectId }: Event['team-project-updated']) { @@ -57,4 +75,82 @@ export class TelemetryEventRelay { role, }); } + + private sourceControlSettingsUpdated({ + branchName, + readOnlyInstance, + repoType, + connected, + }: Event['source-control-settings-updated']) { + void this.telemetry.track('User updated source control settings', { + branch_name: branchName, + read_only_instance: readOnlyInstance, + repo_type: repoType, + connected, + }); + } + + private sourceControlUserStartedPullUi({ + workflowUpdates, + workflowConflicts, + credConflicts, + }: Event['source-control-user-started-pull-ui']) { + void this.telemetry.track('User started pull via UI', { + workflow_updates: workflowUpdates, + workflow_conflicts: workflowConflicts, + cred_conflicts: credConflicts, + }); + } + + private sourceControlUserFinishedPullUi({ + workflowUpdates, + }: Event['source-control-user-finished-pull-ui']) { + void this.telemetry.track('User finished pull via UI', { + workflow_updates: workflowUpdates, + }); + } + + private sourceControlUserPulledApi({ + workflowUpdates, + forced, + }: Event['source-control-user-pulled-api']) { + console.log('source-control-user-pulled-api', { + workflow_updates: workflowUpdates, + forced, + }); + void this.telemetry.track('User pulled via API', { + workflow_updates: workflowUpdates, + forced, + }); + } + + private sourceControlUserStartedPushUi({ + workflowsEligible, + workflowsEligibleWithConflicts, + credsEligible, + credsEligibleWithConflicts, + variablesEligible, + }: Event['source-control-user-started-push-ui']) { + void this.telemetry.track('User started push via UI', { + workflows_eligible: workflowsEligible, + workflows_eligible_with_conflicts: workflowsEligibleWithConflicts, + creds_eligible: credsEligible, + creds_eligible_with_conflicts: credsEligibleWithConflicts, + variables_eligible: variablesEligible, + }); + } + + private sourceControlUserFinishedPushUi({ + workflowsEligible, + workflowsPushed, + credsPushed, + variablesPushed, + }: Event['source-control-user-finished-push-ui']) { + void this.telemetry.track('User finished push via UI', { + workflows_eligible: workflowsEligible, + workflows_pushed: workflowsPushed, + creds_pushed: credsPushed, + variables_pushed: variablesPushed, + }); + } } diff --git a/packages/cli/test/unit/SourceControl.test.ts b/packages/cli/test/unit/SourceControl.test.ts index 4d0d786532..5eeccbb1b9 100644 --- a/packages/cli/test/unit/SourceControl.test.ts +++ b/packages/cli/test/unit/SourceControl.test.ts @@ -218,30 +218,30 @@ describe('Source Control', () => { it('should get tracking information from pre-push results', () => { const trackingResult = getTrackingInformationFromPrePushResult(pushResult); expect(trackingResult).toEqual({ - workflows_eligible: 3, - workflows_eligible_with_conflicts: 1, - creds_eligible: 1, - creds_eligible_with_conflicts: 0, - variables_eligible: 1, + workflowsEligible: 3, + workflowsEligibleWithConflicts: 1, + credsEligible: 1, + credsEligibleWithConflicts: 0, + variablesEligible: 1, }); }); it('should get tracking information from post-push results', () => { const trackingResult = getTrackingInformationFromPostPushResult(pushResult); expect(trackingResult).toEqual({ - workflows_pushed: 2, - workflows_eligible: 3, - creds_pushed: 1, - variables_pushed: 1, + workflowsPushed: 2, + workflowsEligible: 3, + credsPushed: 1, + variablesPushed: 1, }); }); it('should get tracking information from pull results', () => { const trackingResult = getTrackingInformationFromPullResult(pullResult); expect(trackingResult).toEqual({ - cred_conflicts: 1, - workflow_conflicts: 1, - workflow_updates: 3, + credConflicts: 1, + workflowConflicts: 1, + workflowUpdates: 3, }); });