refactor(core): Decouple lifecycle events from internal hooks (no-changelog) (#10305)

This commit is contained in:
Iván Ovejero 2024-08-07 16:09:42 +02:00 committed by GitHub
parent b232831f18
commit 9b977e80f6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 129 additions and 119 deletions

View file

@ -1,7 +1,6 @@
import { Service } from 'typedi'; import { Service } from 'typedi';
import type { ITelemetryTrackProperties } from 'n8n-workflow'; import type { ITelemetryTrackProperties } from 'n8n-workflow';
import type { User } from '@db/entities/User'; import type { User } from '@db/entities/User';
import { WorkflowStatisticsService } from '@/services/workflow-statistics.service';
import { Telemetry } from '@/telemetry'; import { Telemetry } from '@/telemetry';
import { MessageEventBus } from './eventbus/MessageEventBus/MessageEventBus'; import { MessageEventBus } from './eventbus/MessageEventBus/MessageEventBus';
@ -14,28 +13,16 @@ import { MessageEventBus } from './eventbus/MessageEventBus/MessageEventBus';
export class InternalHooks { export class InternalHooks {
constructor( constructor(
private readonly telemetry: Telemetry, private readonly telemetry: Telemetry,
workflowStatisticsService: WorkflowStatisticsService,
// Can't use @ts-expect-error because only dev time tsconfig considers this as an error, but not build time // Can't use @ts-expect-error because only dev time tsconfig considers this as an error, but not build time
// eslint-disable-next-line @typescript-eslint/ban-ts-comment // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore - needed until we decouple telemetry // @ts-ignore - needed until we decouple telemetry
private readonly _eventBus: MessageEventBus, // needed until we decouple telemetry private readonly _eventBus: MessageEventBus, // needed until we decouple telemetry
) { ) {}
workflowStatisticsService.on('telemetry.onFirstProductionWorkflowSuccess', (metrics) =>
this.onFirstProductionWorkflowSuccess(metrics),
);
workflowStatisticsService.on('telemetry.onFirstWorkflowDataLoad', (metrics) =>
this.onFirstWorkflowDataLoad(metrics),
);
}
async init() { async init() {
await this.telemetry.init(); await this.telemetry.init();
} }
onFrontendSettingsAPI(pushRef?: string): void {
this.telemetry.track('Session started', { session_id: pushRef });
}
onWorkflowSharingUpdate(workflowId: string, userId: string, userList: string[]) { onWorkflowSharingUpdate(workflowId: string, userId: string, userList: string[]) {
const properties: ITelemetryTrackProperties = { const properties: ITelemetryTrackProperties = {
workflow_id: workflowId, workflow_id: workflowId,
@ -46,14 +33,6 @@ export class InternalHooks {
this.telemetry.track('User updated workflow sharing', properties); this.telemetry.track('User updated workflow sharing', properties);
} }
async onN8nStop(): Promise<void> {
const timeoutPromise = new Promise<void>((resolve) => {
setTimeout(resolve, 3000);
});
return await Promise.race([timeoutPromise, this.telemetry.trackN8nStop()]);
}
onUserInviteEmailClick(userInviteClickData: { inviter: User; invitee: User }) { onUserInviteEmailClick(userInviteClickData: { inviter: User; invitee: User }) {
this.telemetry.track('User clicked invite link from email', { this.telemetry.track('User clicked invite link from email', {
user_id: userInviteClickData.invitee.id, user_id: userInviteClickData.invitee.id,
@ -85,10 +64,6 @@ export class InternalHooks {
}); });
} }
onInstanceOwnerSetup(instanceOwnerSetupData: { user_id: string }) {
this.telemetry.track('Owner finished instance setup', instanceOwnerSetupData);
}
onEmailFailed(failedEmailData: { onEmailFailed(failedEmailData: {
user: User; user: User;
message_type: message_type:
@ -103,22 +78,4 @@ export class InternalHooks {
user_id: failedEmailData.user.id, user_id: failedEmailData.user.id,
}); });
} }
/*
* Execution Statistics
*/
onFirstProductionWorkflowSuccess(data: { user_id: string; workflow_id: string }) {
this.telemetry.track('Workflow first prod success', data);
}
onFirstWorkflowDataLoad(data: {
user_id: string;
workflow_id: string;
node_type: string;
node_id: string;
credential_type?: string;
credential_id?: string;
}) {
this.telemetry.track('Workflow first data fetched', data);
}
} }

View file

@ -16,7 +16,6 @@ import { ActiveWorkflowManager } from '@/ActiveWorkflowManager';
import { Server } from '@/Server'; import { Server } from '@/Server';
import { EDITOR_UI_DIST_DIR, LICENSE_FEATURES } from '@/constants'; import { EDITOR_UI_DIST_DIR, LICENSE_FEATURES } from '@/constants';
import { MessageEventBus } from '@/eventbus/MessageEventBus/MessageEventBus'; import { MessageEventBus } from '@/eventbus/MessageEventBus/MessageEventBus';
import { InternalHooks } from '@/InternalHooks';
import { License } from '@/License'; import { License } from '@/License';
import { OrchestrationService } from '@/services/orchestration.service'; import { OrchestrationService } from '@/services/orchestration.service';
import { OrchestrationHandlerMainService } from '@/services/orchestration/main/orchestration.handler.main.service'; import { OrchestrationHandlerMainService } from '@/services/orchestration/main/orchestration.handler.main.service';
@ -110,7 +109,7 @@ export class Start extends BaseCommand {
await Container.get(OrchestrationService).shutdown(); await Container.get(OrchestrationService).shutdown();
} }
await Container.get(InternalHooks).onN8nStop(); Container.get(EventService).emit('instance-stopped');
await Container.get(ActiveExecutions).shutdown(); await Container.get(ActiveExecutions).shutdown();

View file

@ -10,7 +10,6 @@ import type { User } from '@db/entities/User';
import type { SettingsRepository } from '@db/repositories/settings.repository'; import type { SettingsRepository } from '@db/repositories/settings.repository';
import type { UserRepository } from '@db/repositories/user.repository'; import type { UserRepository } from '@db/repositories/user.repository';
import { BadRequestError } from '@/errors/response-errors/bad-request.error'; import { BadRequestError } from '@/errors/response-errors/bad-request.error';
import type { InternalHooks } from '@/InternalHooks';
import { License } from '@/License'; import { License } from '@/License';
import type { OwnerRequest } from '@/requests'; import type { OwnerRequest } from '@/requests';
import type { UserService } from '@/services/user.service'; import type { UserService } from '@/services/user.service';
@ -21,7 +20,6 @@ import { badPasswords } from '@test/testData';
describe('OwnerController', () => { describe('OwnerController', () => {
const configGetSpy = jest.spyOn(config, 'getEnv'); const configGetSpy = jest.spyOn(config, 'getEnv');
const internalHooks = mock<InternalHooks>();
const authService = mock<AuthService>(); const authService = mock<AuthService>();
const userService = mock<UserService>(); const userService = mock<UserService>();
const userRepository = mock<UserRepository>(); const userRepository = mock<UserRepository>();
@ -29,7 +27,7 @@ describe('OwnerController', () => {
mockInstance(License).isWithinUsersLimit.mockReturnValue(true); mockInstance(License).isWithinUsersLimit.mockReturnValue(true);
const controller = new OwnerController( const controller = new OwnerController(
mock(), mock(),
internalHooks, mock(),
settingsRepository, settingsRepository,
authService, authService,
userService, userService,

View file

@ -13,13 +13,13 @@ import { PostHogClient } from '@/posthog';
import { UserService } from '@/services/user.service'; import { UserService } from '@/services/user.service';
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';
import { InternalHooks } from '@/InternalHooks'; import { EventService } from '@/events/event.service';
@RestController('/owner') @RestController('/owner')
export class OwnerController { export class OwnerController {
constructor( constructor(
private readonly logger: Logger, private readonly logger: Logger,
private readonly internalHooks: InternalHooks, private readonly eventService: EventService,
private readonly settingsRepository: SettingsRepository, private readonly settingsRepository: SettingsRepository,
private readonly authService: AuthService, private readonly authService: AuthService,
private readonly userService: UserService, private readonly userService: UserService,
@ -85,7 +85,7 @@ export class OwnerController {
this.authService.issueCookie(res, owner, req.browserId); this.authService.issueCookie(res, owner, req.browserId);
this.internalHooks.onInstanceOwnerSetup({ user_id: owner.id }); this.eventService.emit('instance-owner-setup', { userId: owner.id });
return await this.userService.toPublic(owner, { posthog: this.postHog, withScopes: true }); return await this.userService.toPublic(owner, { posthog: this.postHog, withScopes: true });
} }

View file

@ -13,10 +13,35 @@ export type UserLike = {
}; };
export type RelayEventMap = { export type RelayEventMap = {
// #region Server // #region Lifecycle
'server-started': {}; 'server-started': {};
'session-started': {
pushRef?: string;
};
'instance-stopped': {};
'instance-owner-setup': {
userId: string;
};
'first-production-workflow-succeeded': {
projectId: string;
workflowId: string;
userId: string;
};
'first-workflow-data-loaded': {
userId: string;
workflowId: string;
nodeType: string;
nodeId: string;
credentialType?: string;
credentialId?: string;
};
// #endregion // #endregion
// #region Workflow // #region Workflow

View file

@ -73,6 +73,12 @@ export class TelemetryEventRelay extends EventRelay {
'workflow-deleted': (event) => this.workflowDeleted(event), 'workflow-deleted': (event) => this.workflowDeleted(event),
'workflow-saved': async (event) => await this.workflowSaved(event), 'workflow-saved': async (event) => await this.workflowSaved(event),
'server-started': async () => await this.serverStarted(), 'server-started': async () => await this.serverStarted(),
'session-started': (event) => this.sessionStarted(event),
'instance-stopped': () => this.instanceStopped(),
'instance-owner-setup': async (event) => await this.instanceOwnerSetup(event),
'first-production-workflow-succeeded': (event) =>
this.firstProductionWorkflowSucceeded(event),
'first-workflow-data-loaded': (event) => this.firstWorkflowDataLoaded(event),
'workflow-post-execute': async (event) => await this.workflowPostExecute(event), 'workflow-post-execute': async (event) => await this.workflowPostExecute(event),
'user-changed-role': (event) => this.userChangedRole(event), 'user-changed-role': (event) => this.userChangedRole(event),
'user-retrieved-user': (event) => this.userRetrievedUser(event), 'user-retrieved-user': (event) => this.userRetrievedUser(event),
@ -687,7 +693,7 @@ export class TelemetryEventRelay extends EventRelay {
// #endregion // #endregion
// #region Server // #region Lifecycle
private async serverStarted() { private async serverStarted() {
const cpus = os.cpus(); const cpus = os.cpus();
@ -753,6 +759,48 @@ export class TelemetryEventRelay extends EventRelay {
}); });
} }
private sessionStarted({ pushRef }: RelayEventMap['session-started']) {
this.telemetry.track('Session started', { session_id: pushRef });
}
private instanceStopped() {
this.telemetry.track('User instance stopped');
}
private async instanceOwnerSetup({ userId }: RelayEventMap['instance-owner-setup']) {
this.telemetry.track('Owner finished instance setup', { user_id: userId });
}
private firstProductionWorkflowSucceeded({
projectId,
workflowId,
userId,
}: RelayEventMap['first-production-workflow-succeeded']) {
this.telemetry.track('Workflow first prod success', {
project_id: projectId,
workflow_id: workflowId,
user_id: userId,
});
}
private firstWorkflowDataLoaded({
userId,
workflowId,
nodeType,
nodeId,
credentialType,
credentialId,
}: RelayEventMap['first-workflow-data-loaded']) {
this.telemetry.track('Workflow first data fetched', {
user_id: userId,
workflow_id: workflowId,
node_type: nodeType,
node_id: nodeId,
credential_type: credentialType,
credential_id: credentialId,
});
}
// #endregion // #endregion
// #region User // #region User

View file

@ -19,6 +19,7 @@ import { UserService } from '@/services/user.service';
import { OwnershipService } from '@/services/ownership.service'; import { OwnershipService } from '@/services/ownership.service';
import { mockInstance } from '@test/mocking'; import { mockInstance } from '@test/mocking';
import type { Project } from '@/databases/entities/Project'; import type { Project } from '@/databases/entities/Project';
import type { EventService } from '@/events/event.service';
describe('WorkflowStatisticsService', () => { describe('WorkflowStatisticsService', () => {
const fakeUser = mock<User>({ id: 'abcde-fghij' }); const fakeUser = mock<User>({ id: 'abcde-fghij' });
@ -44,21 +45,15 @@ describe('WorkflowStatisticsService', () => {
mocked(ownershipService.getProjectOwnerCached).mockResolvedValue(fakeUser); mocked(ownershipService.getProjectOwnerCached).mockResolvedValue(fakeUser);
const updateSettingsMock = jest.spyOn(userService, 'updateSettings').mockImplementation(); const updateSettingsMock = jest.spyOn(userService, 'updateSettings').mockImplementation();
const eventService = mock<EventService>();
const workflowStatisticsService = new WorkflowStatisticsService( const workflowStatisticsService = new WorkflowStatisticsService(
mock(), mock(),
new WorkflowStatisticsRepository(dataSource, globalConfig), new WorkflowStatisticsRepository(dataSource, globalConfig),
ownershipService, ownershipService,
userService, userService,
eventService,
); );
const onFirstProductionWorkflowSuccess = jest.fn();
const onFirstWorkflowDataLoad = jest.fn();
workflowStatisticsService.on(
'telemetry.onFirstProductionWorkflowSuccess',
onFirstProductionWorkflowSuccess,
);
workflowStatisticsService.on('telemetry.onFirstWorkflowDataLoad', onFirstWorkflowDataLoad);
beforeEach(() => { beforeEach(() => {
jest.clearAllMocks(); jest.clearAllMocks();
}); });
@ -97,11 +92,10 @@ describe('WorkflowStatisticsService', () => {
await workflowStatisticsService.workflowExecutionCompleted(workflow, runData); await workflowStatisticsService.workflowExecutionCompleted(workflow, runData);
expect(updateSettingsMock).toHaveBeenCalledTimes(1); expect(updateSettingsMock).toHaveBeenCalledTimes(1);
expect(onFirstProductionWorkflowSuccess).toBeCalledTimes(1); expect(eventService.emit).toHaveBeenCalledWith('first-production-workflow-succeeded', {
expect(onFirstProductionWorkflowSuccess).toHaveBeenNthCalledWith(1, { projectId: fakeProject.id,
project_id: fakeProject.id, workflowId: workflow.id,
user_id: fakeUser.id, userId: fakeUser.id,
workflow_id: workflow.id,
}); });
}); });
@ -124,7 +118,7 @@ describe('WorkflowStatisticsService', () => {
startedAt: new Date(), startedAt: new Date(),
}; };
await workflowStatisticsService.workflowExecutionCompleted(workflow, runData); await workflowStatisticsService.workflowExecutionCompleted(workflow, runData);
expect(onFirstProductionWorkflowSuccess).toBeCalledTimes(0); expect(eventService.emit).not.toHaveBeenCalled();
}); });
test('should not send metrics for updated entries', async () => { test('should not send metrics for updated entries', async () => {
@ -147,7 +141,7 @@ describe('WorkflowStatisticsService', () => {
}; };
mockDBCall(2); mockDBCall(2);
await workflowStatisticsService.workflowExecutionCompleted(workflow, runData); await workflowStatisticsService.workflowExecutionCompleted(workflow, runData);
expect(onFirstProductionWorkflowSuccess).toBeCalledTimes(0); expect(eventService.emit).not.toHaveBeenCalled();
}); });
}); });
@ -164,13 +158,12 @@ describe('WorkflowStatisticsService', () => {
parameters: {}, parameters: {},
}; };
await workflowStatisticsService.nodeFetchedData(workflowId, node); await workflowStatisticsService.nodeFetchedData(workflowId, node);
expect(onFirstWorkflowDataLoad).toBeCalledTimes(1); expect(eventService.emit).toHaveBeenCalledWith('first-workflow-data-loaded', {
expect(onFirstWorkflowDataLoad).toHaveBeenNthCalledWith(1, { userId: fakeUser.id,
user_id: fakeUser.id, project: fakeProject.id,
project_id: fakeProject.id, workflowId,
workflow_id: workflowId, nodeType: node.type,
node_type: node.type, nodeId: node.id,
node_id: node.id,
}); });
}); });
@ -192,15 +185,14 @@ describe('WorkflowStatisticsService', () => {
}, },
}; };
await workflowStatisticsService.nodeFetchedData(workflowId, node); await workflowStatisticsService.nodeFetchedData(workflowId, node);
expect(onFirstWorkflowDataLoad).toBeCalledTimes(1); expect(eventService.emit).toHaveBeenCalledWith('first-workflow-data-loaded', {
expect(onFirstWorkflowDataLoad).toHaveBeenNthCalledWith(1, { userId: fakeUser.id,
user_id: fakeUser.id, project: fakeProject.id,
project_id: fakeProject.id, workflowId,
workflow_id: workflowId, nodeType: node.type,
node_type: node.type, nodeId: node.id,
node_id: node.id, credentialType: 'testCredentials',
credential_type: 'testCredentials', credentialId: node.credentials.testCredentials.id,
credential_id: node.credentials.testCredentials.id,
}); });
}); });
@ -217,7 +209,7 @@ describe('WorkflowStatisticsService', () => {
parameters: {}, parameters: {},
}; };
await workflowStatisticsService.nodeFetchedData(workflowId, node); await workflowStatisticsService.nodeFetchedData(workflowId, node);
expect(onFirstWorkflowDataLoad).toBeCalledTimes(0); expect(eventService.emit).not.toHaveBeenCalled();
}); });
}); });
}); });

View file

@ -31,7 +31,7 @@ import { UserManagementMailer } from '@/UserManagement/email';
import type { CommunityPackagesService } from '@/services/communityPackages.service'; import type { CommunityPackagesService } from '@/services/communityPackages.service';
import { Logger } from '@/Logger'; import { Logger } from '@/Logger';
import { UrlService } from './url.service'; import { UrlService } from './url.service';
import { InternalHooks } from '@/InternalHooks'; import { EventService } from '@/events/event.service';
import { isApiEnabled } from '@/PublicApi'; import { isApiEnabled } from '@/PublicApi';
@Service() @Service()
@ -50,7 +50,7 @@ export class FrontendService {
private readonly mailer: UserManagementMailer, private readonly mailer: UserManagementMailer,
private readonly instanceSettings: InstanceSettings, private readonly instanceSettings: InstanceSettings,
private readonly urlService: UrlService, private readonly urlService: UrlService,
private readonly internalHooks: InternalHooks, private readonly eventService: EventService,
) { ) {
loadNodesAndCredentials.addPostProcessor(async () => await this.generateTypes()); loadNodesAndCredentials.addPostProcessor(async () => await this.generateTypes());
void this.generateTypes(); void this.generateTypes();
@ -244,7 +244,7 @@ export class FrontendService {
} }
getSettings(pushRef?: string): IN8nUISettings { getSettings(pushRef?: string): IN8nUISettings {
this.internalHooks.onFrontendSettingsAPI(pushRef); this.eventService.emit('session-started', { pushRef });
const restEndpoint = this.globalConfig.endpoints.rest; const restEndpoint = this.globalConfig.endpoints.rest;

View file

@ -6,6 +6,7 @@ import { UserService } from '@/services/user.service';
import { Logger } from '@/Logger'; import { Logger } from '@/Logger';
import { OwnershipService } from './ownership.service'; import { OwnershipService } from './ownership.service';
import { TypedEmitter } from '@/TypedEmitter'; import { TypedEmitter } from '@/TypedEmitter';
import { EventService } from '@/events/event.service';
type WorkflowStatisticsEvents = { type WorkflowStatisticsEvents = {
nodeFetchedData: { workflowId: string; node: INode }; nodeFetchedData: { workflowId: string; node: INode };
@ -31,6 +32,7 @@ export class WorkflowStatisticsService extends TypedEmitter<WorkflowStatisticsEv
private readonly repository: WorkflowStatisticsRepository, private readonly repository: WorkflowStatisticsRepository,
private readonly ownershipService: OwnershipService, private readonly ownershipService: OwnershipService,
private readonly userService: UserService, private readonly userService: UserService,
private readonly eventService: EventService,
) { ) {
super({ captureRejections: true }); super({ captureRejections: true });
if ('SKIP_STATISTICS_EVENTS' in process.env) return; if ('SKIP_STATISTICS_EVENTS' in process.env) return;
@ -72,12 +74,6 @@ export class WorkflowStatisticsService extends TypedEmitter<WorkflowStatisticsEv
if (project.type === 'personal') { if (project.type === 'personal') {
const owner = await this.ownershipService.getProjectOwnerCached(project.id); const owner = await this.ownershipService.getProjectOwnerCached(project.id);
const metrics = {
project_id: project.id,
workflow_id: workflowId,
user_id: owner!.id,
};
if (owner && !owner.settings?.userActivated) { if (owner && !owner.settings?.userActivated) {
await this.userService.updateSettings(owner.id, { await this.userService.updateSettings(owner.id, {
firstSuccessfulWorkflowId: workflowId, firstSuccessfulWorkflowId: workflowId,
@ -86,8 +82,11 @@ export class WorkflowStatisticsService extends TypedEmitter<WorkflowStatisticsEv
}); });
} }
// Send the metrics this.eventService.emit('first-production-workflow-succeeded', {
this.emit('telemetry.onFirstProductionWorkflowSuccess', metrics); projectId: project.id,
workflowId,
userId: owner!.id,
});
} }
} }
} catch (error) { } catch (error) {
@ -109,24 +108,23 @@ export class WorkflowStatisticsService extends TypedEmitter<WorkflowStatisticsEv
const owner = await this.ownershipService.getProjectOwnerCached(project.id); const owner = await this.ownershipService.getProjectOwnerCached(project.id);
let metrics = { let metrics = {
user_id: owner!.id, userId: owner!.id,
project_id: project.id, project: project.id,
workflow_id: workflowId, workflowId,
node_type: node.type, nodeType: node.type,
node_id: node.id, nodeId: node.id,
}; };
// This is probably naive but I can't see a way for a node to have multiple credentials attached so.. // This is probably naive but I can't see a way for a node to have multiple credentials attached so..
if (node.credentials) { if (node.credentials) {
Object.entries(node.credentials).forEach(([credName, credDetails]) => { Object.entries(node.credentials).forEach(([credName, credDetails]) => {
metrics = Object.assign(metrics, { metrics = Object.assign(metrics, {
credential_type: credName, credentialType: credName,
credential_id: credDetails.id, credentialId: credDetails.id,
}); });
}); });
} }
// Send metrics to posthog this.eventService.emit('first-workflow-data-loaded', metrics);
this.emit('telemetry.onFirstWorkflowDataLoad', metrics);
} }
} }

View file

@ -34,7 +34,7 @@ describe('Telemetry', () => {
jest.clearAllTimers(); jest.clearAllTimers();
jest.useRealTimers(); jest.useRealTimers();
startPulseSpy.mockRestore(); startPulseSpy.mockRestore();
await telemetry.trackN8nStop(); await telemetry.stopTracking();
}); });
beforeEach(async () => { beforeEach(async () => {
@ -49,14 +49,7 @@ describe('Telemetry', () => {
}); });
afterEach(async () => { afterEach(async () => {
await telemetry.trackN8nStop(); await telemetry.stopTracking();
});
describe('trackN8nStop', () => {
test('should call track method', async () => {
await telemetry.trackN8nStop();
expect(spyTrack).toHaveBeenCalledTimes(1);
});
}); });
describe('trackWorkflowExecution', () => { describe('trackWorkflowExecution', () => {

View file

@ -9,12 +9,13 @@ import config from '@/config';
import type { IExecutionTrackProperties } from '@/Interfaces'; import type { IExecutionTrackProperties } from '@/Interfaces';
import { Logger } from '@/Logger'; import { Logger } from '@/Logger';
import { License } from '@/License'; import { License } from '@/License';
import { N8N_VERSION } from '@/constants'; import { LOWEST_SHUTDOWN_PRIORITY, N8N_VERSION } from '@/constants';
import { WorkflowRepository } from '@db/repositories/workflow.repository'; import { WorkflowRepository } from '@db/repositories/workflow.repository';
import { SourceControlPreferencesService } from '../environments/sourceControl/sourceControlPreferences.service.ee'; import { SourceControlPreferencesService } from '../environments/sourceControl/sourceControlPreferences.service.ee';
import { UserRepository } from '@db/repositories/user.repository'; import { UserRepository } from '@db/repositories/user.repository';
import { ProjectRepository } from '@/databases/repositories/project.repository'; import { ProjectRepository } from '@/databases/repositories/project.repository';
import { ProjectRelationRepository } from '@/databases/repositories/projectRelation.repository'; import { ProjectRelationRepository } from '@/databases/repositories/projectRelation.repository';
import { OnShutdown } from '@/decorators/OnShutdown';
type ExecutionTrackDataKey = 'manual_error' | 'manual_success' | 'prod_error' | 'prod_success'; type ExecutionTrackDataKey = 'manual_error' | 'manual_success' | 'prod_error' | 'prod_success';
@ -167,11 +168,10 @@ export class Telemetry {
} }
} }
async trackN8nStop(): Promise<void> { @OnShutdown(LOWEST_SHUTDOWN_PRIORITY)
async stopTracking(): Promise<void> {
clearInterval(this.pulseIntervalReference); clearInterval(this.pulseIntervalReference);
this.track('User instance stopped');
await Promise.all([this.postHog.stop(), this.rudderStack?.flush()]); await Promise.all([this.postHog.stop(), this.rudderStack?.flush()]);
} }