🔀 Merge master

This commit is contained in:
Iván Ovejero
2021-12-13 09:50:26 +01:00
72 changed files with 2348 additions and 767 deletions

View File

@@ -13,6 +13,7 @@ import {
} from 'n8n-workflow';
import { ChildProcess } from 'child_process';
import { stringify } from 'flatted';
// eslint-disable-next-line import/no-extraneous-dependencies
import * as PCancelable from 'p-cancelable';
// eslint-disable-next-line import/no-cycle
@@ -82,6 +83,7 @@ export class ActiveExecutions {
const execution = {
id: executionId,
data: stringify(executionData.executionData!),
waitTill: null,
};

View File

@@ -314,7 +314,10 @@ export interface IDiagnosticInfo {
export interface IInternalHooksClass {
onN8nStop(): Promise<void>;
onServerStarted(diagnosticInfo: IDiagnosticInfo): Promise<unknown[]>;
onServerStarted(
diagnosticInfo: IDiagnosticInfo,
firstWorkflowCreatedAt?: Date,
): Promise<unknown[]>;
onPersonalizationSurveySubmitted(answers: IPersonalizationSurveyAnswers): Promise<void>;
onWorkflowCreated(workflow: IWorkflowBase): Promise<void>;
onWorkflowDeleted(workflowId: string): Promise<void>;
@@ -404,10 +407,12 @@ export interface IN8nUISettings {
}
export interface IPersonalizationSurveyAnswers {
companySize: string | null;
codingSkill: string | null;
workArea: string | null;
companyIndustry: string[];
companySize: string | null;
otherCompanyIndustry: string | null;
otherWorkArea: string | null;
workArea: string[] | string | null;
}
export interface IPersonalizationSurvey {

View File

@@ -9,9 +9,16 @@ import {
import { Telemetry } from './telemetry';
export class InternalHooksClass implements IInternalHooksClass {
constructor(private telemetry: Telemetry) {}
private versionCli: string;
async onServerStarted(diagnosticInfo: IDiagnosticInfo): Promise<unknown[]> {
constructor(private telemetry: Telemetry, versionCli: string) {
this.versionCli = versionCli;
}
async onServerStarted(
diagnosticInfo: IDiagnosticInfo,
earliestWorkflowCreatedAt?: Date,
): Promise<unknown[]> {
const info = {
version_cli: diagnosticInfo.versionCli,
db_type: diagnosticInfo.databaseType,
@@ -25,7 +32,10 @@ export class InternalHooksClass implements IInternalHooksClass {
return Promise.all([
this.telemetry.identify(info),
this.telemetry.track('Instance started', info),
this.telemetry.track('Instance started', {
...info,
earliest_workflow_created: earliestWorkflowCreatedAt,
}),
]);
}
@@ -35,13 +45,17 @@ export class InternalHooksClass implements IInternalHooksClass {
coding_skill: answers.codingSkill,
work_area: answers.workArea,
other_work_area: answers.otherWorkArea,
company_industry: answers.companyIndustry,
other_company_industry: answers.otherCompanyIndustry,
});
}
async onWorkflowCreated(workflow: IWorkflowBase): Promise<void> {
const { nodeGraph } = TelemetryHelpers.generateNodesGraph(workflow);
return this.telemetry.track('User created workflow', {
workflow_id: workflow.id,
node_graph: TelemetryHelpers.generateNodesGraph(workflow).nodeGraph,
node_graph: nodeGraph,
node_graph_string: JSON.stringify(nodeGraph),
});
}
@@ -52,9 +66,13 @@ export class InternalHooksClass implements IInternalHooksClass {
}
async onWorkflowSaved(workflow: IWorkflowBase): Promise<void> {
const { nodeGraph } = TelemetryHelpers.generateNodesGraph(workflow);
return this.telemetry.track('User saved workflow', {
workflow_id: workflow.id,
node_graph: TelemetryHelpers.generateNodesGraph(workflow).nodeGraph,
node_graph: nodeGraph,
node_graph_string: JSON.stringify(nodeGraph),
version_cli: this.versionCli,
});
}
@@ -62,6 +80,7 @@ export class InternalHooksClass implements IInternalHooksClass {
const properties: IDataObject = {
workflow_id: workflow.id,
is_manual: false,
version_cli: this.versionCli,
};
if (runData !== undefined) {
@@ -92,6 +111,8 @@ export class InternalHooksClass implements IInternalHooksClass {
if (properties.is_manual) {
const nodeGraphResult = TelemetryHelpers.generateNodesGraph(workflow);
properties.node_graph = nodeGraphResult.nodeGraph;
properties.node_graph_string = JSON.stringify(nodeGraphResult.nodeGraph);
if (errorNodeName) {
properties.error_node_id = nodeGraphResult.nameIndices[errorNodeName];
}

View File

@@ -13,9 +13,12 @@ export class InternalHooksManager {
throw new Error('InternalHooks not initialized');
}
static init(instanceId: string): InternalHooksClass {
static init(instanceId: string, versionCli: string): InternalHooksClass {
if (!this.internalHooksInstance) {
this.internalHooksInstance = new InternalHooksClass(new Telemetry(instanceId));
this.internalHooksInstance = new InternalHooksClass(
new Telemetry(instanceId, versionCli),
versionCli,
);
}
return this.internalHooksInstance;

View File

@@ -2952,7 +2952,23 @@ export async function start(): Promise<void> {
deploymentType: config.get('deployment.type'),
};
void InternalHooksManager.getInstance().onServerStarted(diagnosticInfo);
void Db.collections
.Workflow!.findOne({
select: ['createdAt'],
order: { createdAt: 'ASC' },
})
.then(async (workflow) =>
InternalHooksManager.getInstance().onServerStarted(diagnosticInfo, workflow?.createdAt),
);
});
server.on('error', (error: Error & { code: string }) => {
if (error.code === 'EADDRINUSE') {
console.log(
`n8n's port ${PORT} is already in use. Do you have another instance of n8n running already?`,
);
process.exit(1);
}
});
}

View File

@@ -509,7 +509,7 @@ function hookFunctionsSave(parentProcessMode?: string): IWorkflowExecuteHooks {
this.workflowData,
fullRunData,
this.mode,
undefined,
this.executionId,
this.retryOf,
);
}
@@ -585,7 +585,7 @@ function hookFunctionsSave(parentProcessMode?: string): IWorkflowExecuteHooks {
this.workflowData,
fullRunData,
this.mode,
undefined,
this.executionId,
this.retryOf,
);
}
@@ -635,7 +635,7 @@ function hookFunctionsSaveWorker(): IWorkflowExecuteHooks {
this.workflowData,
fullRunData,
this.mode,
undefined,
this.executionId,
this.retryOf,
);
}
@@ -676,7 +676,13 @@ function hookFunctionsSaveWorker(): IWorkflowExecuteHooks {
});
}
} catch (error) {
executeErrorWorkflow(this.workflowData, fullRunData, this.mode, undefined, this.retryOf);
executeErrorWorkflow(
this.workflowData,
fullRunData,
this.mode,
this.executionId,
this.retryOf,
);
}
},
],

View File

@@ -31,6 +31,7 @@ import {
CredentialTypes,
Db,
ExternalHooks,
GenericHelpers,
IWorkflowExecuteProcess,
IWorkflowExecutionDataProcessWithExecution,
NodeTypes,
@@ -137,7 +138,8 @@ export class WorkflowRunnerProcess {
await externalHooks.init();
const instanceId = (await UserSettings.prepareUserSettings()).instanceId ?? '';
InternalHooksManager.init(instanceId);
const { cli } = await GenericHelpers.getVersions();
InternalHooksManager.init(instanceId, cli);
// Credentials should now be loaded from database.
// We check if any node uses credentials. If it does, then

View File

@@ -5,28 +5,57 @@ import { IDataObject, LoggerProxy } from 'n8n-workflow';
import config = require('../../config');
import { getLogger } from '../Logger';
interface IExecutionCountsBufferItem {
manual_success_count: number;
manual_error_count: number;
prod_success_count: number;
prod_error_count: number;
}
type CountBufferItemKey =
| 'manual_success_count'
| 'manual_error_count'
| 'prod_success_count'
| 'prod_error_count';
type FirstExecutionItemKey =
| 'first_manual_success'
| 'first_manual_error'
| 'first_prod_success'
| 'first_prod_error';
type IExecutionCountsBufferItem = {
[key in CountBufferItemKey]: number;
};
interface IExecutionCountsBuffer {
[workflowId: string]: IExecutionCountsBufferItem;
}
type IFirstExecutions = {
[key in FirstExecutionItemKey]: Date | undefined;
};
interface IExecutionsBuffer {
counts: IExecutionCountsBuffer;
firstExecutions: IFirstExecutions;
}
export class Telemetry {
private client?: TelemetryClient;
private instanceId: string;
private versionCli: string;
private pulseIntervalReference: NodeJS.Timeout;
private executionCountsBuffer: IExecutionCountsBuffer = {};
private executionCountsBuffer: IExecutionsBuffer = {
counts: {},
firstExecutions: {
first_manual_error: undefined,
first_manual_success: undefined,
first_prod_error: undefined,
first_prod_success: undefined,
},
};
constructor(instanceId: string) {
constructor(instanceId: string, versionCli: string) {
this.instanceId = instanceId;
this.versionCli = versionCli;
const enabled = config.get('diagnostics.enabled') as boolean;
if (enabled) {
@@ -53,33 +82,41 @@ export class Telemetry {
return Promise.resolve();
}
const allPromises = Object.keys(this.executionCountsBuffer).map(async (workflowId) => {
const allPromises = Object.keys(this.executionCountsBuffer.counts).map(async (workflowId) => {
const promise = this.track('Workflow execution count', {
version_cli: this.versionCli,
workflow_id: workflowId,
...this.executionCountsBuffer[workflowId],
...this.executionCountsBuffer.counts[workflowId],
...this.executionCountsBuffer.firstExecutions,
});
this.executionCountsBuffer[workflowId].manual_error_count = 0;
this.executionCountsBuffer[workflowId].manual_success_count = 0;
this.executionCountsBuffer[workflowId].prod_error_count = 0;
this.executionCountsBuffer[workflowId].prod_success_count = 0;
this.executionCountsBuffer.counts[workflowId].manual_error_count = 0;
this.executionCountsBuffer.counts[workflowId].manual_success_count = 0;
this.executionCountsBuffer.counts[workflowId].prod_error_count = 0;
this.executionCountsBuffer.counts[workflowId].prod_success_count = 0;
return promise;
});
allPromises.push(this.track('pulse'));
allPromises.push(this.track('pulse', { version_cli: this.versionCli }));
return Promise.all(allPromises);
}
async trackWorkflowExecution(properties: IDataObject): Promise<void> {
if (this.client) {
const workflowId = properties.workflow_id as string;
this.executionCountsBuffer[workflowId] = this.executionCountsBuffer[workflowId] ?? {
this.executionCountsBuffer.counts[workflowId] = this.executionCountsBuffer.counts[
workflowId
] ?? {
manual_error_count: 0,
manual_success_count: 0,
prod_error_count: 0,
prod_success_count: 0,
};
let countKey: CountBufferItemKey;
let firstExecKey: FirstExecutionItemKey;
if (
properties.success === false &&
properties.error_node_type &&
@@ -89,15 +126,28 @@ export class Telemetry {
void this.track('Workflow execution errored', properties);
if (properties.is_manual) {
this.executionCountsBuffer[workflowId].manual_error_count++;
firstExecKey = 'first_manual_error';
countKey = 'manual_error_count';
} else {
this.executionCountsBuffer[workflowId].prod_error_count++;
firstExecKey = 'first_prod_error';
countKey = 'prod_error_count';
}
} else if (properties.is_manual) {
this.executionCountsBuffer[workflowId].manual_success_count++;
countKey = 'manual_success_count';
firstExecKey = 'first_manual_success';
} else {
this.executionCountsBuffer[workflowId].prod_success_count++;
countKey = 'prod_success_count';
firstExecKey = 'first_prod_success';
}
if (
!this.executionCountsBuffer.firstExecutions[firstExecKey] &&
this.executionCountsBuffer.counts[workflowId][countKey] === 0
) {
this.executionCountsBuffer.firstExecutions[firstExecKey] = new Date();
}
this.executionCountsBuffer.counts[workflowId][countKey]++;
}
}