mirror of
https://github.com/n8n-io/n8n.git
synced 2024-12-24 20:24:05 -08:00
refactor(core): Decouple lifecycle events from internal hooks (no-changelog) (#10305)
This commit is contained in:
parent
b232831f18
commit
9b977e80f6
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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 });
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
@ -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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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', () => {
|
||||||
|
|
|
@ -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()]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue