mirror of
https://github.com/n8n-io/n8n.git
synced 2025-01-11 21:07:28 -08:00
refactor(core): Decouple server started event from internal hooks (no-changelog) (#10221)
This commit is contained in:
parent
e2ee91569a
commit
24ffca7c75
|
@ -1,8 +1,6 @@
|
||||||
import { Service } from 'typedi';
|
import { Service } from 'typedi';
|
||||||
import { snakeCase } from 'change-case';
|
import { snakeCase } from 'change-case';
|
||||||
import os from 'node:os';
|
|
||||||
import { get as pslGet } from 'psl';
|
import { get as pslGet } from 'psl';
|
||||||
import { GlobalConfig } from '@n8n/config';
|
|
||||||
import type {
|
import type {
|
||||||
ExecutionStatus,
|
ExecutionStatus,
|
||||||
INodesGraphResult,
|
INodesGraphResult,
|
||||||
|
@ -11,27 +9,23 @@ import type {
|
||||||
IWorkflowBase,
|
IWorkflowBase,
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
import { TelemetryHelpers } from 'n8n-workflow';
|
import { TelemetryHelpers } from 'n8n-workflow';
|
||||||
import { InstanceSettings } from 'n8n-core';
|
|
||||||
|
|
||||||
import config from '@/config';
|
import config from '@/config';
|
||||||
import { N8N_VERSION } from '@/constants';
|
import { N8N_VERSION } from '@/constants';
|
||||||
import type { AuthProviderType } from '@db/entities/AuthIdentity';
|
import type { AuthProviderType } from '@db/entities/AuthIdentity';
|
||||||
import type { User } from '@db/entities/User';
|
import type { User } from '@db/entities/User';
|
||||||
import { SharedWorkflowRepository } from '@db/repositories/sharedWorkflow.repository';
|
import { SharedWorkflowRepository } from '@db/repositories/sharedWorkflow.repository';
|
||||||
import { WorkflowRepository } from '@db/repositories/workflow.repository';
|
|
||||||
import { determineFinalExecutionStatus } from '@/executionLifecycleHooks/shared/sharedHookFunctions';
|
import { determineFinalExecutionStatus } from '@/executionLifecycleHooks/shared/sharedHookFunctions';
|
||||||
import type {
|
import type {
|
||||||
ITelemetryUserDeletionData,
|
ITelemetryUserDeletionData,
|
||||||
IWorkflowDb,
|
IWorkflowDb,
|
||||||
IExecutionTrackProperties,
|
IExecutionTrackProperties,
|
||||||
} from '@/Interfaces';
|
} from '@/Interfaces';
|
||||||
import { License } from '@/License';
|
|
||||||
import { WorkflowStatisticsService } from '@/services/workflow-statistics.service';
|
import { WorkflowStatisticsService } from '@/services/workflow-statistics.service';
|
||||||
import { NodeTypes } from '@/NodeTypes';
|
import { NodeTypes } from '@/NodeTypes';
|
||||||
import { Telemetry } from '@/telemetry';
|
import { Telemetry } from '@/telemetry';
|
||||||
import type { Project } from '@db/entities/Project';
|
import type { Project } from '@db/entities/Project';
|
||||||
import { ProjectRelationRepository } from './databases/repositories/projectRelation.repository';
|
import { ProjectRelationRepository } from './databases/repositories/projectRelation.repository';
|
||||||
import { SharedCredentialsRepository } from './databases/repositories/sharedCredentials.repository';
|
|
||||||
import { MessageEventBus } from './eventbus/MessageEventBus/MessageEventBus';
|
import { MessageEventBus } from './eventbus/MessageEventBus/MessageEventBus';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -42,16 +36,11 @@ import { MessageEventBus } from './eventbus/MessageEventBus/MessageEventBus';
|
||||||
@Service()
|
@Service()
|
||||||
export class InternalHooks {
|
export class InternalHooks {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly globalConfig: GlobalConfig,
|
|
||||||
private readonly telemetry: Telemetry,
|
private readonly telemetry: Telemetry,
|
||||||
private readonly nodeTypes: NodeTypes,
|
private readonly nodeTypes: NodeTypes,
|
||||||
private readonly sharedWorkflowRepository: SharedWorkflowRepository,
|
private readonly sharedWorkflowRepository: SharedWorkflowRepository,
|
||||||
private readonly workflowRepository: WorkflowRepository,
|
|
||||||
workflowStatisticsService: WorkflowStatisticsService,
|
workflowStatisticsService: WorkflowStatisticsService,
|
||||||
private readonly instanceSettings: InstanceSettings,
|
|
||||||
private readonly license: License,
|
|
||||||
private readonly projectRelationRepository: ProjectRelationRepository,
|
private readonly projectRelationRepository: ProjectRelationRepository,
|
||||||
private readonly sharedCredentialsRepository: SharedCredentialsRepository,
|
|
||||||
private readonly _eventBus: MessageEventBus, // needed until we decouple telemetry
|
private readonly _eventBus: MessageEventBus, // needed until we decouple telemetry
|
||||||
) {
|
) {
|
||||||
workflowStatisticsService.on(
|
workflowStatisticsService.on(
|
||||||
|
@ -68,73 +57,6 @@ export class InternalHooks {
|
||||||
await this.telemetry.init();
|
await this.telemetry.init();
|
||||||
}
|
}
|
||||||
|
|
||||||
async onServerStarted(): Promise<unknown[]> {
|
|
||||||
const cpus = os.cpus();
|
|
||||||
const binaryDataConfig = config.getEnv('binaryDataManager');
|
|
||||||
|
|
||||||
const isS3Selected = config.getEnv('binaryDataManager.mode') === 's3';
|
|
||||||
const isS3Available = config.getEnv('binaryDataManager.availableModes').includes('s3');
|
|
||||||
const isS3Licensed = this.license.isBinaryDataS3Licensed();
|
|
||||||
const authenticationMethod = config.getEnv('userManagement.authenticationMethod');
|
|
||||||
|
|
||||||
const info = {
|
|
||||||
version_cli: N8N_VERSION,
|
|
||||||
db_type: this.globalConfig.database.type,
|
|
||||||
n8n_version_notifications_enabled: this.globalConfig.versionNotifications.enabled,
|
|
||||||
n8n_disable_production_main_process: config.getEnv(
|
|
||||||
'endpoints.disableProductionWebhooksOnMainProcess',
|
|
||||||
),
|
|
||||||
system_info: {
|
|
||||||
os: {
|
|
||||||
type: os.type(),
|
|
||||||
version: os.version(),
|
|
||||||
},
|
|
||||||
memory: os.totalmem() / 1024,
|
|
||||||
cpus: {
|
|
||||||
count: cpus.length,
|
|
||||||
model: cpus[0].model,
|
|
||||||
speed: cpus[0].speed,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
execution_variables: {
|
|
||||||
executions_mode: config.getEnv('executions.mode'),
|
|
||||||
executions_timeout: config.getEnv('executions.timeout'),
|
|
||||||
executions_timeout_max: config.getEnv('executions.maxTimeout'),
|
|
||||||
executions_data_save_on_error: config.getEnv('executions.saveDataOnError'),
|
|
||||||
executions_data_save_on_success: config.getEnv('executions.saveDataOnSuccess'),
|
|
||||||
executions_data_save_on_progress: config.getEnv('executions.saveExecutionProgress'),
|
|
||||||
executions_data_save_manual_executions: config.getEnv(
|
|
||||||
'executions.saveDataManualExecutions',
|
|
||||||
),
|
|
||||||
executions_data_prune: config.getEnv('executions.pruneData'),
|
|
||||||
executions_data_max_age: config.getEnv('executions.pruneDataMaxAge'),
|
|
||||||
},
|
|
||||||
n8n_deployment_type: config.getEnv('deployment.type'),
|
|
||||||
n8n_binary_data_mode: binaryDataConfig.mode,
|
|
||||||
smtp_set_up: this.globalConfig.userManagement.emails.mode === 'smtp',
|
|
||||||
ldap_allowed: authenticationMethod === 'ldap',
|
|
||||||
saml_enabled: authenticationMethod === 'saml',
|
|
||||||
license_plan_name: this.license.getPlanName(),
|
|
||||||
license_tenant_id: config.getEnv('license.tenantId'),
|
|
||||||
binary_data_s3: isS3Available && isS3Selected && isS3Licensed,
|
|
||||||
multi_main_setup_enabled: config.getEnv('multiMainSetup.enabled'),
|
|
||||||
};
|
|
||||||
|
|
||||||
const firstWorkflow = await this.workflowRepository.findOne({
|
|
||||||
select: ['createdAt'],
|
|
||||||
order: { createdAt: 'ASC' },
|
|
||||||
where: {},
|
|
||||||
});
|
|
||||||
|
|
||||||
return await Promise.all([
|
|
||||||
this.telemetry.identify(info),
|
|
||||||
this.telemetry.track('Instance started', {
|
|
||||||
...info,
|
|
||||||
earliest_workflow_created: firstWorkflow?.createdAt,
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
async onFrontendSettingsAPI(pushRef?: string): Promise<void> {
|
async onFrontendSettingsAPI(pushRef?: string): Promise<void> {
|
||||||
return await this.telemetry.track('Session started', { session_id: pushRef });
|
return await this.telemetry.track('Session started', { session_id: pushRef });
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,7 +33,6 @@ import { isLdapEnabled } from '@/Ldap/helpers.ee';
|
||||||
import { AbstractServer } from '@/AbstractServer';
|
import { AbstractServer } from '@/AbstractServer';
|
||||||
import { PostHogClient } from '@/posthog';
|
import { PostHogClient } from '@/posthog';
|
||||||
import { MessageEventBus } from '@/eventbus/MessageEventBus/MessageEventBus';
|
import { MessageEventBus } from '@/eventbus/MessageEventBus/MessageEventBus';
|
||||||
import { InternalHooks } from '@/InternalHooks';
|
|
||||||
import { handleMfaDisable, isMfaFeatureEnabled } from '@/Mfa/helpers';
|
import { handleMfaDisable, isMfaFeatureEnabled } from '@/Mfa/helpers';
|
||||||
import type { FrontendService } from '@/services/frontend.service';
|
import type { FrontendService } from '@/services/frontend.service';
|
||||||
import { OrchestrationService } from '@/services/orchestration.service';
|
import { OrchestrationService } from '@/services/orchestration.service';
|
||||||
|
@ -66,6 +65,7 @@ import '@/ExternalSecrets/ExternalSecrets.controller.ee';
|
||||||
import '@/license/license.controller';
|
import '@/license/license.controller';
|
||||||
import '@/workflows/workflowHistory/workflowHistory.controller.ee';
|
import '@/workflows/workflowHistory/workflowHistory.controller.ee';
|
||||||
import '@/workflows/workflows.controller';
|
import '@/workflows/workflows.controller';
|
||||||
|
import { EventService } from './eventbus/event.service';
|
||||||
|
|
||||||
const exec = promisify(callbackExec);
|
const exec = promisify(callbackExec);
|
||||||
|
|
||||||
|
@ -82,6 +82,7 @@ export class Server extends AbstractServer {
|
||||||
private readonly orchestrationService: OrchestrationService,
|
private readonly orchestrationService: OrchestrationService,
|
||||||
private readonly postHogClient: PostHogClient,
|
private readonly postHogClient: PostHogClient,
|
||||||
private readonly globalConfig: GlobalConfig,
|
private readonly globalConfig: GlobalConfig,
|
||||||
|
private readonly eventService: EventService,
|
||||||
) {
|
) {
|
||||||
super('main');
|
super('main');
|
||||||
|
|
||||||
|
@ -106,7 +107,7 @@ export class Server extends AbstractServer {
|
||||||
void this.loadNodesAndCredentials.setupHotReload();
|
void this.loadNodesAndCredentials.setupHotReload();
|
||||||
}
|
}
|
||||||
|
|
||||||
void Container.get(InternalHooks).onServerStarted();
|
this.eventService.emit('server-started');
|
||||||
}
|
}
|
||||||
|
|
||||||
private async registerAdditionalControllers() {
|
private async registerAdditionalControllers() {
|
||||||
|
|
|
@ -15,6 +15,8 @@ export type UserLike = {
|
||||||
* Events sent by `EventService` and forwarded by relays, e.g. `AuditEventRelay` and `TelemetryEventRelay`.
|
* Events sent by `EventService` and forwarded by relays, e.g. `AuditEventRelay` and `TelemetryEventRelay`.
|
||||||
*/
|
*/
|
||||||
export type Event = {
|
export type Event = {
|
||||||
|
'server-started': {};
|
||||||
|
|
||||||
'workflow-created': {
|
'workflow-created': {
|
||||||
user: UserLike;
|
user: UserLike;
|
||||||
workflow: IWorkflowBase;
|
workflow: IWorkflowBase;
|
||||||
|
|
|
@ -3,12 +3,20 @@ import { EventService } from '@/eventbus/event.service';
|
||||||
import type { Event } from '@/eventbus/event.types';
|
import type { Event } from '@/eventbus/event.types';
|
||||||
import { Telemetry } from '.';
|
import { Telemetry } from '.';
|
||||||
import config from '@/config';
|
import config from '@/config';
|
||||||
|
import os from 'node:os';
|
||||||
|
import { License } from '@/License';
|
||||||
|
import { GlobalConfig } from '@n8n/config';
|
||||||
|
import { N8N_VERSION } from '@/constants';
|
||||||
|
import { WorkflowRepository } from '@/databases/repositories/workflow.repository';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export class TelemetryEventRelay {
|
export class TelemetryEventRelay {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly eventService: EventService,
|
private readonly eventService: EventService,
|
||||||
private readonly telemetry: Telemetry,
|
private readonly telemetry: Telemetry,
|
||||||
|
private readonly license: License,
|
||||||
|
private readonly globalConfig: GlobalConfig,
|
||||||
|
private readonly workflowRepository: WorkflowRepository,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async init() {
|
async init() {
|
||||||
|
@ -20,6 +28,8 @@ export class TelemetryEventRelay {
|
||||||
}
|
}
|
||||||
|
|
||||||
private setupHandlers() {
|
private setupHandlers() {
|
||||||
|
this.eventService.on('server-started', async () => await this.serverStarted());
|
||||||
|
|
||||||
this.eventService.on('team-project-updated', (event) => this.teamProjectUpdated(event));
|
this.eventService.on('team-project-updated', (event) => this.teamProjectUpdated(event));
|
||||||
this.eventService.on('team-project-deleted', (event) => this.teamProjectDeleted(event));
|
this.eventService.on('team-project-deleted', (event) => this.teamProjectDeleted(event));
|
||||||
this.eventService.on('team-project-created', (event) => this.teamProjectCreated(event));
|
this.eventService.on('team-project-created', (event) => this.teamProjectCreated(event));
|
||||||
|
@ -420,4 +430,71 @@ export class TelemetryEventRelay {
|
||||||
private loginFailedDueToLdapDisabled({ userId }: Event['login-failed-due-to-ldap-disabled']) {
|
private loginFailedDueToLdapDisabled({ userId }: Event['login-failed-due-to-ldap-disabled']) {
|
||||||
void this.telemetry.track('User login failed since ldap disabled', { user_ud: userId });
|
void this.telemetry.track('User login failed since ldap disabled', { user_ud: userId });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async serverStarted() {
|
||||||
|
const cpus = os.cpus();
|
||||||
|
const binaryDataConfig = config.getEnv('binaryDataManager');
|
||||||
|
|
||||||
|
const isS3Selected = config.getEnv('binaryDataManager.mode') === 's3';
|
||||||
|
const isS3Available = config.getEnv('binaryDataManager.availableModes').includes('s3');
|
||||||
|
const isS3Licensed = this.license.isBinaryDataS3Licensed();
|
||||||
|
const authenticationMethod = config.getEnv('userManagement.authenticationMethod');
|
||||||
|
|
||||||
|
const info = {
|
||||||
|
version_cli: N8N_VERSION,
|
||||||
|
db_type: this.globalConfig.database.type,
|
||||||
|
n8n_version_notifications_enabled: this.globalConfig.versionNotifications.enabled,
|
||||||
|
n8n_disable_production_main_process: config.getEnv(
|
||||||
|
'endpoints.disableProductionWebhooksOnMainProcess',
|
||||||
|
),
|
||||||
|
system_info: {
|
||||||
|
os: {
|
||||||
|
type: os.type(),
|
||||||
|
version: os.version(),
|
||||||
|
},
|
||||||
|
memory: os.totalmem() / 1024,
|
||||||
|
cpus: {
|
||||||
|
count: cpus.length,
|
||||||
|
model: cpus[0].model,
|
||||||
|
speed: cpus[0].speed,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
execution_variables: {
|
||||||
|
executions_mode: config.getEnv('executions.mode'),
|
||||||
|
executions_timeout: config.getEnv('executions.timeout'),
|
||||||
|
executions_timeout_max: config.getEnv('executions.maxTimeout'),
|
||||||
|
executions_data_save_on_error: config.getEnv('executions.saveDataOnError'),
|
||||||
|
executions_data_save_on_success: config.getEnv('executions.saveDataOnSuccess'),
|
||||||
|
executions_data_save_on_progress: config.getEnv('executions.saveExecutionProgress'),
|
||||||
|
executions_data_save_manual_executions: config.getEnv(
|
||||||
|
'executions.saveDataManualExecutions',
|
||||||
|
),
|
||||||
|
executions_data_prune: config.getEnv('executions.pruneData'),
|
||||||
|
executions_data_max_age: config.getEnv('executions.pruneDataMaxAge'),
|
||||||
|
},
|
||||||
|
n8n_deployment_type: config.getEnv('deployment.type'),
|
||||||
|
n8n_binary_data_mode: binaryDataConfig.mode,
|
||||||
|
smtp_set_up: this.globalConfig.userManagement.emails.mode === 'smtp',
|
||||||
|
ldap_allowed: authenticationMethod === 'ldap',
|
||||||
|
saml_enabled: authenticationMethod === 'saml',
|
||||||
|
license_plan_name: this.license.getPlanName(),
|
||||||
|
license_tenant_id: config.getEnv('license.tenantId'),
|
||||||
|
binary_data_s3: isS3Available && isS3Selected && isS3Licensed,
|
||||||
|
multi_main_setup_enabled: config.getEnv('multiMainSetup.enabled'),
|
||||||
|
};
|
||||||
|
|
||||||
|
const firstWorkflow = await this.workflowRepository.findOne({
|
||||||
|
select: ['createdAt'],
|
||||||
|
order: { createdAt: 'ASC' },
|
||||||
|
where: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
void Promise.all([
|
||||||
|
this.telemetry.identify(info),
|
||||||
|
this.telemetry.track('Instance started', {
|
||||||
|
...info,
|
||||||
|
earliest_workflow_created: firstWorkflow?.createdAt,
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,97 +0,0 @@
|
||||||
import { mock } from 'jest-mock-extended';
|
|
||||||
import type { GlobalConfig } from '@n8n/config';
|
|
||||||
import { N8N_VERSION } from '@/constants';
|
|
||||||
import { InternalHooks } from '@/InternalHooks';
|
|
||||||
import type { License } from '@/License';
|
|
||||||
import type { Telemetry } from '@/telemetry';
|
|
||||||
|
|
||||||
jest.mock('node:os', () => ({
|
|
||||||
tmpdir: () => '',
|
|
||||||
cpus: () => [{ model: 'MIPS R3000', speed: 40_000_000 }],
|
|
||||||
type: () => 'TempleOS',
|
|
||||||
version: () => '5.03',
|
|
||||||
totalmem: () => 1024 * 1024,
|
|
||||||
}));
|
|
||||||
|
|
||||||
describe('InternalHooks', () => {
|
|
||||||
const telemetry = mock<Telemetry>();
|
|
||||||
const license = mock<License>();
|
|
||||||
const globalConfig = mock<GlobalConfig>({
|
|
||||||
database: {
|
|
||||||
type: 'sqlite',
|
|
||||||
},
|
|
||||||
userManagement: {
|
|
||||||
emails: {
|
|
||||||
mode: 'smtp',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
versionNotifications: {
|
|
||||||
enabled: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const internalHooks = new InternalHooks(
|
|
||||||
globalConfig,
|
|
||||||
telemetry,
|
|
||||||
mock(),
|
|
||||||
mock(),
|
|
||||||
mock(),
|
|
||||||
mock(),
|
|
||||||
mock(),
|
|
||||||
license,
|
|
||||||
mock(),
|
|
||||||
mock(),
|
|
||||||
mock(),
|
|
||||||
);
|
|
||||||
|
|
||||||
beforeEach(() => jest.clearAllMocks());
|
|
||||||
|
|
||||||
it('Should be defined', () => {
|
|
||||||
expect(internalHooks).toBeDefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('Should forward license plan name and tenant id to identify when provided', async () => {
|
|
||||||
license.getPlanName.mockReturnValue('Best Plan');
|
|
||||||
globalConfig.database.type = 'sqlite';
|
|
||||||
|
|
||||||
await internalHooks.onServerStarted();
|
|
||||||
|
|
||||||
expect(telemetry.identify).toHaveBeenCalledWith({
|
|
||||||
version_cli: N8N_VERSION,
|
|
||||||
db_type: 'sqlite',
|
|
||||||
n8n_version_notifications_enabled: true,
|
|
||||||
n8n_disable_production_main_process: false,
|
|
||||||
system_info: {
|
|
||||||
memory: 1024,
|
|
||||||
os: {
|
|
||||||
type: 'TempleOS',
|
|
||||||
version: '5.03',
|
|
||||||
},
|
|
||||||
cpus: {
|
|
||||||
count: 1,
|
|
||||||
model: 'MIPS R3000',
|
|
||||||
speed: 40000000,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
execution_variables: {
|
|
||||||
executions_data_max_age: 336,
|
|
||||||
executions_data_prune: true,
|
|
||||||
executions_data_save_manual_executions: true,
|
|
||||||
executions_data_save_on_error: 'all',
|
|
||||||
executions_data_save_on_progress: false,
|
|
||||||
executions_data_save_on_success: 'all',
|
|
||||||
executions_mode: 'regular',
|
|
||||||
executions_timeout: -1,
|
|
||||||
executions_timeout_max: 3600,
|
|
||||||
},
|
|
||||||
n8n_deployment_type: 'default',
|
|
||||||
n8n_binary_data_mode: 'default',
|
|
||||||
smtp_set_up: true,
|
|
||||||
ldap_allowed: false,
|
|
||||||
saml_enabled: false,
|
|
||||||
license_plan_name: 'Best Plan',
|
|
||||||
license_tenant_id: 1,
|
|
||||||
binary_data_s3: false,
|
|
||||||
multi_main_setup_enabled: false,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
Loading…
Reference in a new issue