refactor(core): Decouple source control telemetry from internal hooks (no-changelog) (#10095)

This commit is contained in:
Iván Ovejero 2024-07-18 15:00:24 +02:00 committed by GitHub
parent 028a8a2c75
commit f876f9ec8b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 184 additions and 110 deletions

View file

@ -775,55 +775,6 @@ export class InternalHooks {
return await this.telemetry.track('User created variable', createData); 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<void> {
return await this.telemetry.track('User updated source control settings', data);
}
async onSourceControlUserStartedPullUI(data: {
workflow_updates: number;
workflow_conflicts: number;
cred_conflicts: number;
}): Promise<void> {
return await this.telemetry.track('User started pull via UI', data);
}
async onSourceControlUserFinishedPullUI(data: { workflow_updates: number }): Promise<void> {
return await this.telemetry.track('User finished pull via UI', {
workflow_updates: data.workflow_updates,
});
}
async onSourceControlUserPulledAPI(data: {
workflow_updates: number;
forced: boolean;
}): Promise<void> {
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<void> {
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<void> {
return await this.telemetry.track('User finished push via UI', data);
}
async onExternalSecretsProviderSettingsSaved(saveData: { async onExternalSecretsProviderSettingsSaved(saveData: {
user_id?: string | undefined; user_id?: string | undefined;
vault_type: string; vault_type: string;

View file

@ -10,7 +10,7 @@ import {
getTrackingInformationFromPullResult, getTrackingInformationFromPullResult,
isSourceControlLicensed, isSourceControlLicensed,
} from '@/environments/sourceControl/sourceControlHelper.ee'; } from '@/environments/sourceControl/sourceControlHelper.ee';
import { InternalHooks } from '@/InternalHooks'; import { EventRelay } from '@/eventbus/event-relay.service';
export = { export = {
pull: [ pull: [
@ -39,7 +39,7 @@ export = {
}); });
if (result.statusCode === 200) { if (result.statusCode === 200) {
void Container.get(InternalHooks).onSourceControlUserPulledAPI({ Container.get(EventRelay).emit('source-control-user-pulled-api', {
...getTrackingInformationFromPullResult(result.statusResult), ...getTrackingInformationFromPullResult(result.statusResult),
forced: req.body.force ?? false, forced: req.body.force ?? false,
}); });

View file

@ -12,7 +12,7 @@ import type { SourceControlPreferences } from './types/sourceControlPreferences'
import type { SourceControlledFile } from './types/sourceControlledFile'; import type { SourceControlledFile } from './types/sourceControlledFile';
import { SOURCE_CONTROL_DEFAULT_BRANCH } from './constants'; import { SOURCE_CONTROL_DEFAULT_BRANCH } from './constants';
import type { ImportResult } from './types/importResult'; import type { ImportResult } from './types/importResult';
import { InternalHooks } from '@/InternalHooks'; import { EventRelay } from '@/eventbus/event-relay.service';
import { getRepoType } from './sourceControlHelper.ee'; import { getRepoType } from './sourceControlHelper.ee';
import { SourceControlGetStatus } from './types/sourceControlGetStatus'; import { SourceControlGetStatus } from './types/sourceControlGetStatus';
import { BadRequestError } from '@/errors/response-errors/bad-request.error'; import { BadRequestError } from '@/errors/response-errors/bad-request.error';
@ -22,7 +22,7 @@ export class SourceControlController {
constructor( constructor(
private readonly sourceControlService: SourceControlService, private readonly sourceControlService: SourceControlService,
private readonly sourceControlPreferencesService: SourceControlPreferencesService, private readonly sourceControlPreferencesService: SourceControlPreferencesService,
private readonly internalHooks: InternalHooks, private readonly eventRelay: EventRelay,
) {} ) {}
@Get('/preferences', { middlewares: [sourceControlLicensedMiddleware], skipAuth: true }) @Get('/preferences', { middlewares: [sourceControlLicensedMiddleware], skipAuth: true })
@ -83,11 +83,11 @@ export class SourceControlController {
const resultingPreferences = this.sourceControlPreferencesService.getPreferences(); const resultingPreferences = this.sourceControlPreferencesService.getPreferences();
// #region Tracking Information // #region Tracking Information
// located in controller so as to not call this multiple times when updating preferences // located in controller so as to not call this multiple times when updating preferences
void this.internalHooks.onSourceControlSettingsUpdated({ this.eventRelay.emit('source-control-settings-updated', {
branch_name: resultingPreferences.branchName, branchName: resultingPreferences.branchName,
connected: resultingPreferences.connected, connected: resultingPreferences.connected,
read_only_instance: resultingPreferences.branchReadOnly, readOnlyInstance: resultingPreferences.branchReadOnly,
repo_type: getRepoType(resultingPreferences.repositoryUrl), repoType: getRepoType(resultingPreferences.repositoryUrl),
}); });
// #endregion // #endregion
return resultingPreferences; return resultingPreferences;
@ -128,11 +128,11 @@ export class SourceControlController {
} }
await this.sourceControlService.init(); await this.sourceControlService.init();
const resultingPreferences = this.sourceControlPreferencesService.getPreferences(); const resultingPreferences = this.sourceControlPreferencesService.getPreferences();
void this.internalHooks.onSourceControlSettingsUpdated({ this.eventRelay.emit('source-control-settings-updated', {
branch_name: resultingPreferences.branchName, branchName: resultingPreferences.branchName,
connected: resultingPreferences.connected, connected: resultingPreferences.connected,
read_only_instance: resultingPreferences.branchReadOnly, readOnlyInstance: resultingPreferences.branchReadOnly,
repo_type: getRepoType(resultingPreferences.repositoryUrl), repoType: getRepoType(resultingPreferences.repositoryUrl),
}); });
return resultingPreferences; return resultingPreferences;
} catch (error) { } catch (error) {

View file

@ -1,4 +1,4 @@
import Container, { Service } from 'typedi'; import { Service } from 'typedi';
import path from 'path'; import path from 'path';
import { import {
getTagsPath, getTagsPath,
@ -30,7 +30,7 @@ import type { TagEntity } from '@db/entities/TagEntity';
import type { Variables } from '@db/entities/Variables'; import type { Variables } from '@db/entities/Variables';
import type { SourceControlWorkflowVersionId } from './types/sourceControlWorkflowVersionId'; import type { SourceControlWorkflowVersionId } from './types/sourceControlWorkflowVersionId';
import type { ExportableCredential } from './types/exportableCredential'; 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 { TagRepository } from '@db/repositories/tag.repository';
import { Logger } from '@/Logger'; import { Logger } from '@/Logger';
import { BadRequestError } from '@/errors/response-errors/bad-request.error'; import { BadRequestError } from '@/errors/response-errors/bad-request.error';
@ -52,6 +52,7 @@ export class SourceControlService {
private sourceControlExportService: SourceControlExportService, private sourceControlExportService: SourceControlExportService,
private sourceControlImportService: SourceControlImportService, private sourceControlImportService: SourceControlImportService,
private tagRepository: TagRepository, private tagRepository: TagRepository,
private readonly eventRelay: EventRelay,
) { ) {
const { gitFolder, sshFolder, sshKeyName } = sourceControlPreferencesService; const { gitFolder, sshFolder, sshKeyName } = sourceControlPreferencesService;
this.gitFolder = gitFolder; this.gitFolder = gitFolder;
@ -291,7 +292,8 @@ export class SourceControlService {
}); });
// #region Tracking Information // #region Tracking Information
void Container.get(InternalHooks).onSourceControlUserFinishedPushUI( this.eventRelay.emit(
'source-control-user-finished-push-ui',
getTrackingInformationFromPostPushResult(statusResult), getTrackingInformationFromPostPushResult(statusResult),
); );
// #endregion // #endregion
@ -368,7 +370,8 @@ export class SourceControlService {
} }
// #region Tracking Information // #region Tracking Information
void Container.get(InternalHooks).onSourceControlUserFinishedPullUI( this.eventRelay.emit(
'source-control-user-finished-pull-ui',
getTrackingInformationFromPullResult(statusResult), getTrackingInformationFromPullResult(statusResult),
); );
// #endregion // #endregion
@ -421,11 +424,13 @@ export class SourceControlService {
// #region Tracking Information // #region Tracking Information
if (options.direction === 'push') { if (options.direction === 'push') {
void Container.get(InternalHooks).onSourceControlUserStartedPushUI( this.eventRelay.emit(
'source-control-user-started-push-ui',
getTrackingInformationFromPrePushResult(sourceControlledFiles), getTrackingInformationFromPrePushResult(sourceControlledFiles),
); );
} else if (options.direction === 'pull') { } else if (options.direction === 'pull') {
void Container.get(InternalHooks).onSourceControlUserStartedPullUI( this.eventRelay.emit(
'source-control-user-started-pull-ui',
getTrackingInformationFromPullResult(sourceControlledFiles), getTrackingInformationFromPullResult(sourceControlledFiles),
); );
} }

View file

@ -121,58 +121,43 @@ function filterSourceControlledFilesUniqueIds(files: SourceControlledFile[]) {
); );
} }
export function getTrackingInformationFromPullResult(result: SourceControlledFile[]): { export function getTrackingInformationFromPullResult(result: SourceControlledFile[]) {
cred_conflicts: number;
workflow_conflicts: number;
workflow_updates: number;
} {
const uniques = filterSourceControlledFilesUniqueIds(result); const uniques = filterSourceControlledFilesUniqueIds(result);
return { return {
cred_conflicts: uniques.filter( credConflicts: uniques.filter(
(file) => (file) =>
file.type === 'credential' && file.status === 'modified' && file.location === 'local', file.type === 'credential' && file.status === 'modified' && file.location === 'local',
).length, ).length,
workflow_conflicts: uniques.filter( workflowConflicts: uniques.filter(
(file) => file.type === 'workflow' && file.status === 'modified' && file.location === 'local', (file) => file.type === 'workflow' && file.status === 'modified' && file.location === 'local',
).length, ).length,
workflow_updates: uniques.filter((file) => file.type === 'workflow').length, workflowUpdates: uniques.filter((file) => file.type === 'workflow').length,
}; };
} }
export function getTrackingInformationFromPrePushResult(result: SourceControlledFile[]): { export function getTrackingInformationFromPrePushResult(result: SourceControlledFile[]) {
workflows_eligible: number;
workflows_eligible_with_conflicts: number;
creds_eligible: number;
creds_eligible_with_conflicts: number;
variables_eligible: number;
} {
const uniques = filterSourceControlledFilesUniqueIds(result); const uniques = filterSourceControlledFilesUniqueIds(result);
return { return {
workflows_eligible: uniques.filter((file) => file.type === 'workflow').length, workflowsEligible: uniques.filter((file) => file.type === 'workflow').length,
workflows_eligible_with_conflicts: uniques.filter( workflowsEligibleWithConflicts: uniques.filter(
(file) => file.type === 'workflow' && file.conflict, (file) => file.type === 'workflow' && file.conflict,
).length, ).length,
creds_eligible: uniques.filter((file) => file.type === 'credential').length, credsEligible: uniques.filter((file) => file.type === 'credential').length,
creds_eligible_with_conflicts: uniques.filter( credsEligibleWithConflicts: uniques.filter(
(file) => file.type === 'credential' && file.conflict, (file) => file.type === 'credential' && file.conflict,
).length, ).length,
variables_eligible: uniques.filter((file) => file.type === 'variables').length, variablesEligible: uniques.filter((file) => file.type === 'variables').length,
}; };
} }
export function getTrackingInformationFromPostPushResult(result: SourceControlledFile[]): { export function getTrackingInformationFromPostPushResult(result: SourceControlledFile[]) {
workflows_eligible: number;
workflows_pushed: number;
creds_pushed: number;
variables_pushed: number;
} {
const uniques = filterSourceControlledFilesUniqueIds(result); const uniques = filterSourceControlledFilesUniqueIds(result);
return { return {
workflows_pushed: uniques.filter((file) => file.pushed && file.type === 'workflow').length ?? 0, workflowsPushed: uniques.filter((file) => file.pushed && file.type === 'workflow').length ?? 0,
workflows_eligible: uniques.filter((file) => file.type === 'workflow').length ?? 0, workflowsEligible: uniques.filter((file) => file.type === 'workflow').length ?? 0,
creds_pushed: credsPushed:
uniques.filter((file) => file.pushed && file.file.startsWith('credential_stubs')).length ?? 0, 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, uniques.filter((file) => file.pushed && file.file.startsWith('variable_stubs')).length ?? 0,
}; };
} }

View file

@ -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 = { export type Event = {
'workflow-created': { 'workflow-created': {
@ -215,4 +215,41 @@ export type Event = {
userId: string; userId: string;
role: GlobalRole; 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;
};
}; };

View file

@ -23,6 +23,24 @@ export class TelemetryEventRelay {
this.eventRelay.on('team-project-updated', (event) => this.teamProjectUpdated(event)); 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-deleted', (event) => this.teamProjectDeleted(event));
this.eventRelay.on('team-project-created', (event) => this.teamProjectCreated(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']) { private teamProjectUpdated({ userId, role, members, projectId }: Event['team-project-updated']) {
@ -57,4 +75,82 @@ export class TelemetryEventRelay {
role, 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,
});
}
} }

View file

@ -218,30 +218,30 @@ describe('Source Control', () => {
it('should get tracking information from pre-push results', () => { it('should get tracking information from pre-push results', () => {
const trackingResult = getTrackingInformationFromPrePushResult(pushResult); const trackingResult = getTrackingInformationFromPrePushResult(pushResult);
expect(trackingResult).toEqual({ expect(trackingResult).toEqual({
workflows_eligible: 3, workflowsEligible: 3,
workflows_eligible_with_conflicts: 1, workflowsEligibleWithConflicts: 1,
creds_eligible: 1, credsEligible: 1,
creds_eligible_with_conflicts: 0, credsEligibleWithConflicts: 0,
variables_eligible: 1, variablesEligible: 1,
}); });
}); });
it('should get tracking information from post-push results', () => { it('should get tracking information from post-push results', () => {
const trackingResult = getTrackingInformationFromPostPushResult(pushResult); const trackingResult = getTrackingInformationFromPostPushResult(pushResult);
expect(trackingResult).toEqual({ expect(trackingResult).toEqual({
workflows_pushed: 2, workflowsPushed: 2,
workflows_eligible: 3, workflowsEligible: 3,
creds_pushed: 1, credsPushed: 1,
variables_pushed: 1, variablesPushed: 1,
}); });
}); });
it('should get tracking information from pull results', () => { it('should get tracking information from pull results', () => {
const trackingResult = getTrackingInformationFromPullResult(pullResult); const trackingResult = getTrackingInformationFromPullResult(pullResult);
expect(trackingResult).toEqual({ expect(trackingResult).toEqual({
cred_conflicts: 1, credConflicts: 1,
workflow_conflicts: 1, workflowConflicts: 1,
workflow_updates: 3, workflowUpdates: 3,
}); });
}); });