mirror of
https://github.com/n8n-io/n8n.git
synced 2025-01-11 12:57:29 -08:00
refactor(core): Decouple post workflow execute event from internal hooks (no-changelog) (#10280)
This commit is contained in:
parent
07d7b247f0
commit
a533916628
|
@ -1,23 +1,10 @@
|
||||||
import { Service } from 'typedi';
|
import { Service } from 'typedi';
|
||||||
import { snakeCase } from 'change-case';
|
import { snakeCase } from 'change-case';
|
||||||
import { get as pslGet } from 'psl';
|
import type { ITelemetryTrackProperties } from 'n8n-workflow';
|
||||||
import type {
|
|
||||||
ExecutionStatus,
|
|
||||||
INodesGraphResult,
|
|
||||||
IRun,
|
|
||||||
ITelemetryTrackProperties,
|
|
||||||
IWorkflowBase,
|
|
||||||
} from 'n8n-workflow';
|
|
||||||
import { TelemetryHelpers } from 'n8n-workflow';
|
|
||||||
|
|
||||||
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 type { ITelemetryUserDeletionData } from '@/Interfaces';
|
||||||
import { determineFinalExecutionStatus } from '@/executionLifecycleHooks/shared/sharedHookFunctions';
|
|
||||||
import type { ITelemetryUserDeletionData, IExecutionTrackProperties } from '@/Interfaces';
|
|
||||||
import { WorkflowStatisticsService } from '@/services/workflow-statistics.service';
|
import { WorkflowStatisticsService } from '@/services/workflow-statistics.service';
|
||||||
import { NodeTypes } from '@/NodeTypes';
|
|
||||||
import { Telemetry } from '@/telemetry';
|
import { Telemetry } from '@/telemetry';
|
||||||
import { MessageEventBus } from './eventbus/MessageEventBus/MessageEventBus';
|
import { MessageEventBus } from './eventbus/MessageEventBus/MessageEventBus';
|
||||||
|
|
||||||
|
@ -30,8 +17,6 @@ import { MessageEventBus } from './eventbus/MessageEventBus/MessageEventBus';
|
||||||
export class InternalHooks {
|
export class InternalHooks {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly telemetry: Telemetry,
|
private readonly telemetry: Telemetry,
|
||||||
private readonly nodeTypes: NodeTypes,
|
|
||||||
private readonly sharedWorkflowRepository: SharedWorkflowRepository,
|
|
||||||
workflowStatisticsService: WorkflowStatisticsService,
|
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
|
||||||
|
@ -64,145 +49,6 @@ export class InternalHooks {
|
||||||
this.telemetry.track('User responded to personalization questions', personalizationSurveyData);
|
this.telemetry.track('User responded to personalization questions', personalizationSurveyData);
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line complexity
|
|
||||||
async onWorkflowPostExecute(
|
|
||||||
_executionId: string,
|
|
||||||
workflow: IWorkflowBase,
|
|
||||||
runData?: IRun,
|
|
||||||
userId?: string,
|
|
||||||
) {
|
|
||||||
if (!workflow.id) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (runData?.status === 'waiting') {
|
|
||||||
// No need to send telemetry or logs when the workflow hasn't finished yet.
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const telemetryProperties: IExecutionTrackProperties = {
|
|
||||||
workflow_id: workflow.id,
|
|
||||||
is_manual: false,
|
|
||||||
version_cli: N8N_VERSION,
|
|
||||||
success: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (userId) {
|
|
||||||
telemetryProperties.user_id = userId;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (runData?.data.resultData.error?.message?.includes('canceled')) {
|
|
||||||
runData.status = 'canceled';
|
|
||||||
}
|
|
||||||
|
|
||||||
telemetryProperties.success = !!runData?.finished;
|
|
||||||
|
|
||||||
// const executionStatus: ExecutionStatus = runData?.status ?? 'unknown';
|
|
||||||
const executionStatus: ExecutionStatus = runData
|
|
||||||
? determineFinalExecutionStatus(runData)
|
|
||||||
: 'unknown';
|
|
||||||
|
|
||||||
if (runData !== undefined) {
|
|
||||||
telemetryProperties.execution_mode = runData.mode;
|
|
||||||
telemetryProperties.is_manual = runData.mode === 'manual';
|
|
||||||
|
|
||||||
let nodeGraphResult: INodesGraphResult | null = null;
|
|
||||||
|
|
||||||
if (!telemetryProperties.success && runData?.data.resultData.error) {
|
|
||||||
telemetryProperties.error_message = runData?.data.resultData.error.message;
|
|
||||||
let errorNodeName =
|
|
||||||
'node' in runData?.data.resultData.error
|
|
||||||
? runData?.data.resultData.error.node?.name
|
|
||||||
: undefined;
|
|
||||||
telemetryProperties.error_node_type =
|
|
||||||
'node' in runData?.data.resultData.error
|
|
||||||
? runData?.data.resultData.error.node?.type
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
if (runData.data.resultData.lastNodeExecuted) {
|
|
||||||
const lastNode = TelemetryHelpers.getNodeTypeForName(
|
|
||||||
workflow,
|
|
||||||
runData.data.resultData.lastNodeExecuted,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (lastNode !== undefined) {
|
|
||||||
telemetryProperties.error_node_type = lastNode.type;
|
|
||||||
errorNodeName = lastNode.name;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (telemetryProperties.is_manual) {
|
|
||||||
nodeGraphResult = TelemetryHelpers.generateNodesGraph(workflow, this.nodeTypes);
|
|
||||||
telemetryProperties.node_graph = nodeGraphResult.nodeGraph;
|
|
||||||
telemetryProperties.node_graph_string = JSON.stringify(nodeGraphResult.nodeGraph);
|
|
||||||
|
|
||||||
if (errorNodeName) {
|
|
||||||
telemetryProperties.error_node_id = nodeGraphResult.nameIndices[errorNodeName];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (telemetryProperties.is_manual) {
|
|
||||||
if (!nodeGraphResult) {
|
|
||||||
nodeGraphResult = TelemetryHelpers.generateNodesGraph(workflow, this.nodeTypes);
|
|
||||||
}
|
|
||||||
|
|
||||||
let userRole: 'owner' | 'sharee' | undefined = undefined;
|
|
||||||
if (userId) {
|
|
||||||
const role = await this.sharedWorkflowRepository.findSharingRole(userId, workflow.id);
|
|
||||||
if (role) {
|
|
||||||
userRole = role === 'workflow:owner' ? 'owner' : 'sharee';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const manualExecEventProperties: ITelemetryTrackProperties = {
|
|
||||||
user_id: userId,
|
|
||||||
workflow_id: workflow.id,
|
|
||||||
status: executionStatus,
|
|
||||||
executionStatus: runData?.status ?? 'unknown',
|
|
||||||
error_message: telemetryProperties.error_message as string,
|
|
||||||
error_node_type: telemetryProperties.error_node_type,
|
|
||||||
node_graph_string: telemetryProperties.node_graph_string as string,
|
|
||||||
error_node_id: telemetryProperties.error_node_id as string,
|
|
||||||
webhook_domain: null,
|
|
||||||
sharing_role: userRole,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!manualExecEventProperties.node_graph_string) {
|
|
||||||
nodeGraphResult = TelemetryHelpers.generateNodesGraph(workflow, this.nodeTypes);
|
|
||||||
manualExecEventProperties.node_graph_string = JSON.stringify(nodeGraphResult.nodeGraph);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (runData.data.startData?.destinationNode) {
|
|
||||||
const telemetryPayload = {
|
|
||||||
...manualExecEventProperties,
|
|
||||||
node_type: TelemetryHelpers.getNodeTypeForName(
|
|
||||||
workflow,
|
|
||||||
runData.data.startData?.destinationNode,
|
|
||||||
)?.type,
|
|
||||||
node_id: nodeGraphResult.nameIndices[runData.data.startData?.destinationNode],
|
|
||||||
};
|
|
||||||
|
|
||||||
this.telemetry.track('Manual node exec finished', telemetryPayload);
|
|
||||||
} else {
|
|
||||||
nodeGraphResult.webhookNodeNames.forEach((name: string) => {
|
|
||||||
const execJson = runData.data.resultData.runData[name]?.[0]?.data?.main?.[0]?.[0]
|
|
||||||
?.json as { headers?: { origin?: string } };
|
|
||||||
if (execJson?.headers?.origin && execJson.headers.origin !== '') {
|
|
||||||
manualExecEventProperties.webhook_domain = pslGet(
|
|
||||||
execJson.headers.origin.replace(/^https?:\/\//, ''),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
this.telemetry.track('Manual workflow exec finished', manualExecEventProperties);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.telemetry.trackWorkflowExecution(telemetryProperties);
|
|
||||||
}
|
|
||||||
|
|
||||||
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,
|
||||||
|
|
|
@ -52,7 +52,6 @@ import { Push } from '@/push';
|
||||||
import * as WorkflowHelpers from '@/WorkflowHelpers';
|
import * as WorkflowHelpers from '@/WorkflowHelpers';
|
||||||
import { findSubworkflowStart, isWorkflowIdValid } from '@/utils';
|
import { findSubworkflowStart, isWorkflowIdValid } from '@/utils';
|
||||||
import { PermissionChecker } from './UserManagement/PermissionChecker';
|
import { PermissionChecker } from './UserManagement/PermissionChecker';
|
||||||
import { InternalHooks } from '@/InternalHooks';
|
|
||||||
import { ExecutionRepository } from '@db/repositories/execution.repository';
|
import { ExecutionRepository } from '@db/repositories/execution.repository';
|
||||||
import { WorkflowStatisticsService } from '@/services/workflow-statistics.service';
|
import { WorkflowStatisticsService } from '@/services/workflow-statistics.service';
|
||||||
import { SecretsHelper } from './SecretsHelpers';
|
import { SecretsHelper } from './SecretsHelpers';
|
||||||
|
@ -548,7 +547,6 @@ function hookFunctionsSave(): IWorkflowExecuteHooks {
|
||||||
*/
|
*/
|
||||||
function hookFunctionsSaveWorker(): IWorkflowExecuteHooks {
|
function hookFunctionsSaveWorker(): IWorkflowExecuteHooks {
|
||||||
const logger = Container.get(Logger);
|
const logger = Container.get(Logger);
|
||||||
const internalHooks = Container.get(InternalHooks);
|
|
||||||
const workflowStatisticsService = Container.get(WorkflowStatisticsService);
|
const workflowStatisticsService = Container.get(WorkflowStatisticsService);
|
||||||
const eventService = Container.get(EventService);
|
const eventService = Container.get(EventService);
|
||||||
return {
|
return {
|
||||||
|
@ -644,13 +642,9 @@ function hookFunctionsSaveWorker(): IWorkflowExecuteHooks {
|
||||||
async function (this: WorkflowHooks, runData: IRun): Promise<void> {
|
async function (this: WorkflowHooks, runData: IRun): Promise<void> {
|
||||||
const { executionId, workflowData: workflow } = this;
|
const { executionId, workflowData: workflow } = this;
|
||||||
|
|
||||||
void internalHooks.onWorkflowPostExecute(executionId, workflow, runData);
|
|
||||||
eventService.emit('workflow-post-execute', {
|
eventService.emit('workflow-post-execute', {
|
||||||
workflowId: workflow.id,
|
workflow,
|
||||||
workflowName: workflow.name,
|
|
||||||
executionId,
|
executionId,
|
||||||
success: runData.status === 'success',
|
|
||||||
isManual: runData.mode === 'manual',
|
|
||||||
runData,
|
runData,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
@ -787,7 +781,6 @@ async function executeWorkflow(
|
||||||
parentCallbackManager?: CallbackManager;
|
parentCallbackManager?: CallbackManager;
|
||||||
},
|
},
|
||||||
): Promise<Array<INodeExecutionData[] | null> | IWorkflowExecuteProcess> {
|
): Promise<Array<INodeExecutionData[] | null> | IWorkflowExecuteProcess> {
|
||||||
const internalHooks = Container.get(InternalHooks);
|
|
||||||
const externalHooks = Container.get(ExternalHooks);
|
const externalHooks = Container.get(ExternalHooks);
|
||||||
await externalHooks.init();
|
await externalHooks.init();
|
||||||
|
|
||||||
|
@ -933,13 +926,9 @@ async function executeWorkflow(
|
||||||
|
|
||||||
await externalHooks.run('workflow.postExecute', [data, workflowData, executionId]);
|
await externalHooks.run('workflow.postExecute', [data, workflowData, executionId]);
|
||||||
|
|
||||||
void internalHooks.onWorkflowPostExecute(executionId, workflowData, data, additionalData.userId);
|
|
||||||
eventService.emit('workflow-post-execute', {
|
eventService.emit('workflow-post-execute', {
|
||||||
workflowId: workflowData.id,
|
workflow: workflowData,
|
||||||
workflowName: workflowData.name,
|
|
||||||
executionId,
|
executionId,
|
||||||
success: data.status === 'success',
|
|
||||||
isManual: data.mode === 'manual',
|
|
||||||
userId: additionalData.userId,
|
userId: additionalData.userId,
|
||||||
runData: data,
|
runData: data,
|
||||||
});
|
});
|
||||||
|
|
|
@ -34,7 +34,6 @@ import * as WorkflowHelpers from '@/WorkflowHelpers';
|
||||||
import * as WorkflowExecuteAdditionalData from '@/WorkflowExecuteAdditionalData';
|
import * as WorkflowExecuteAdditionalData from '@/WorkflowExecuteAdditionalData';
|
||||||
import { generateFailedExecutionFromError } from '@/WorkflowHelpers';
|
import { generateFailedExecutionFromError } from '@/WorkflowHelpers';
|
||||||
import { PermissionChecker } from '@/UserManagement/PermissionChecker';
|
import { PermissionChecker } from '@/UserManagement/PermissionChecker';
|
||||||
import { InternalHooks } from '@/InternalHooks';
|
|
||||||
import { Logger } from '@/Logger';
|
import { Logger } from '@/Logger';
|
||||||
import { WorkflowStaticDataService } from '@/workflows/workflowStaticData.service';
|
import { WorkflowStaticDataService } from '@/workflows/workflowStaticData.service';
|
||||||
import { EventService } from './eventbus/event.service';
|
import { EventService } from './eventbus/event.service';
|
||||||
|
@ -160,18 +159,9 @@ export class WorkflowRunner {
|
||||||
const postExecutePromise = this.activeExecutions.getPostExecutePromise(executionId);
|
const postExecutePromise = this.activeExecutions.getPostExecutePromise(executionId);
|
||||||
postExecutePromise
|
postExecutePromise
|
||||||
.then(async (executionData) => {
|
.then(async (executionData) => {
|
||||||
void Container.get(InternalHooks).onWorkflowPostExecute(
|
|
||||||
executionId,
|
|
||||||
data.workflowData,
|
|
||||||
executionData,
|
|
||||||
data.userId,
|
|
||||||
);
|
|
||||||
this.eventService.emit('workflow-post-execute', {
|
this.eventService.emit('workflow-post-execute', {
|
||||||
workflowId: data.workflowData.id,
|
workflow: data.workflowData,
|
||||||
workflowName: data.workflowData.name,
|
|
||||||
executionId,
|
executionId,
|
||||||
success: executionData?.status === 'success',
|
|
||||||
isManual: data.executionMode === 'manual',
|
|
||||||
userId: data.userId,
|
userId: data.userId,
|
||||||
runData: executionData,
|
runData: executionData,
|
||||||
});
|
});
|
||||||
|
|
|
@ -141,27 +141,31 @@ describe('AuditEventRelay', () => {
|
||||||
it('should log on `workflow-post-execute` for successful execution', () => {
|
it('should log on `workflow-post-execute` for successful execution', () => {
|
||||||
const payload = mock<Event['workflow-post-execute']>({
|
const payload = mock<Event['workflow-post-execute']>({
|
||||||
executionId: 'some-id',
|
executionId: 'some-id',
|
||||||
success: true,
|
|
||||||
userId: 'some-id',
|
userId: 'some-id',
|
||||||
workflowId: 'some-id',
|
workflow: mock<IWorkflowBase>({ id: 'some-id', name: 'some-name' }),
|
||||||
isManual: true,
|
runData: mock<IRun>({ status: 'success', mode: 'manual', data: { resultData: {} } }),
|
||||||
workflowName: 'some-name',
|
|
||||||
metadata: {},
|
|
||||||
runData: mock<IRun>({ data: { resultData: {} } }),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
eventService.emit('workflow-post-execute', payload);
|
eventService.emit('workflow-post-execute', payload);
|
||||||
|
|
||||||
const { runData: _, ...rest } = payload;
|
const { runData: _, workflow: __, ...rest } = payload;
|
||||||
|
|
||||||
expect(eventBus.sendWorkflowEvent).toHaveBeenCalledWith({
|
expect(eventBus.sendWorkflowEvent).toHaveBeenCalledWith({
|
||||||
eventName: 'n8n.workflow.success',
|
eventName: 'n8n.workflow.success',
|
||||||
payload: rest,
|
payload: {
|
||||||
|
...rest,
|
||||||
|
success: true,
|
||||||
|
isManual: true,
|
||||||
|
workflowName: 'some-name',
|
||||||
|
workflowId: 'some-id',
|
||||||
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle `workflow-post-execute` event for unsuccessful execution', () => {
|
it('should log on `workflow-post-execute` event for unsuccessful execution', () => {
|
||||||
const runData = mock<IRun>({
|
const runData = mock<IRun>({
|
||||||
|
status: 'error',
|
||||||
|
mode: 'manual',
|
||||||
data: {
|
data: {
|
||||||
resultData: {
|
resultData: {
|
||||||
lastNodeExecuted: 'some-node',
|
lastNodeExecuted: 'some-node',
|
||||||
|
@ -177,23 +181,23 @@ describe('AuditEventRelay', () => {
|
||||||
|
|
||||||
const event = {
|
const event = {
|
||||||
executionId: 'some-id',
|
executionId: 'some-id',
|
||||||
success: false,
|
|
||||||
userId: 'some-id',
|
userId: 'some-id',
|
||||||
workflowId: 'some-id',
|
workflow: mock<IWorkflowBase>({ id: 'some-id', name: 'some-name' }),
|
||||||
isManual: true,
|
|
||||||
workflowName: 'some-name',
|
|
||||||
metadata: {},
|
|
||||||
runData,
|
runData,
|
||||||
};
|
};
|
||||||
|
|
||||||
eventService.emit('workflow-post-execute', event);
|
eventService.emit('workflow-post-execute', event);
|
||||||
|
|
||||||
const { runData: _, ...rest } = event;
|
const { runData: _, workflow: __, ...rest } = event;
|
||||||
|
|
||||||
expect(eventBus.sendWorkflowEvent).toHaveBeenCalledWith({
|
expect(eventBus.sendWorkflowEvent).toHaveBeenCalledWith({
|
||||||
eventName: 'n8n.workflow.failed',
|
eventName: 'n8n.workflow.failed',
|
||||||
payload: {
|
payload: {
|
||||||
...rest,
|
...rest,
|
||||||
|
success: false,
|
||||||
|
isManual: true,
|
||||||
|
workflowName: 'some-name',
|
||||||
|
workflowId: 'some-id',
|
||||||
lastNodeExecuted: 'some-node',
|
lastNodeExecuted: 'some-node',
|
||||||
errorNodeType: 'some-type',
|
errorNodeType: 'some-type',
|
||||||
errorMessage: 'some-message',
|
errorMessage: 'some-message',
|
||||||
|
|
|
@ -122,12 +122,20 @@ export class AuditEventRelay {
|
||||||
}
|
}
|
||||||
|
|
||||||
private workflowPostExecute(event: Event['workflow-post-execute']) {
|
private workflowPostExecute(event: Event['workflow-post-execute']) {
|
||||||
const { runData, ...rest } = event;
|
const { runData, workflow, ...rest } = event;
|
||||||
|
|
||||||
if (event.success) {
|
const payload = {
|
||||||
|
...rest,
|
||||||
|
success: runData?.status === 'success',
|
||||||
|
isManual: runData?.mode === 'manual',
|
||||||
|
workflowId: workflow.id,
|
||||||
|
workflowName: workflow.name,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (payload.success) {
|
||||||
void this.eventBus.sendWorkflowEvent({
|
void this.eventBus.sendWorkflowEvent({
|
||||||
eventName: 'n8n.workflow.success',
|
eventName: 'n8n.workflow.success',
|
||||||
payload: rest,
|
payload,
|
||||||
});
|
});
|
||||||
|
|
||||||
return;
|
return;
|
||||||
|
@ -136,7 +144,7 @@ export class AuditEventRelay {
|
||||||
void this.eventBus.sendWorkflowEvent({
|
void this.eventBus.sendWorkflowEvent({
|
||||||
eventName: 'n8n.workflow.failed',
|
eventName: 'n8n.workflow.failed',
|
||||||
payload: {
|
payload: {
|
||||||
...rest,
|
...payload,
|
||||||
lastNodeExecuted: runData?.data.resultData.lastNodeExecuted,
|
lastNodeExecuted: runData?.data.resultData.lastNodeExecuted,
|
||||||
errorNodeType:
|
errorNodeType:
|
||||||
runData?.data.resultData.error && 'node' in runData?.data.resultData.error
|
runData?.data.resultData.error && 'node' in runData?.data.resultData.error
|
||||||
|
|
|
@ -44,12 +44,8 @@ export type Event = {
|
||||||
|
|
||||||
'workflow-post-execute': {
|
'workflow-post-execute': {
|
||||||
executionId: string;
|
executionId: string;
|
||||||
success: boolean;
|
|
||||||
userId?: string;
|
userId?: string;
|
||||||
workflowId: string;
|
workflow: IWorkflowBase;
|
||||||
isManual: boolean;
|
|
||||||
workflowName: string;
|
|
||||||
metadata?: Record<string, string>;
|
|
||||||
runData?: IRun;
|
runData?: IRun;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,6 @@ import { Push } from '@/push';
|
||||||
import { jsonStringify, sleep } from 'n8n-workflow';
|
import { jsonStringify, sleep } from 'n8n-workflow';
|
||||||
import { ExecutionRepository } from '@db/repositories/execution.repository';
|
import { ExecutionRepository } from '@db/repositories/execution.repository';
|
||||||
import { getWorkflowHooksMain } from '@/WorkflowExecuteAdditionalData'; // @TODO: Dependency cycle
|
import { getWorkflowHooksMain } from '@/WorkflowExecuteAdditionalData'; // @TODO: Dependency cycle
|
||||||
import { InternalHooks } from '@/InternalHooks'; // @TODO: Dependency cycle if injected
|
|
||||||
import type { DateTime } from 'luxon';
|
import type { DateTime } from 'luxon';
|
||||||
import type { IRun, ITaskData } from 'n8n-workflow';
|
import type { IRun, ITaskData } from 'n8n-workflow';
|
||||||
import type { EventMessageTypes } from '../eventbus/EventMessageClasses';
|
import type { EventMessageTypes } from '../eventbus/EventMessageClasses';
|
||||||
|
@ -280,22 +279,9 @@ export class ExecutionRecoveryService {
|
||||||
private async runHooks(execution: IExecutionResponse) {
|
private async runHooks(execution: IExecutionResponse) {
|
||||||
execution.data ??= { resultData: { runData: {} } };
|
execution.data ??= { resultData: { runData: {} } };
|
||||||
|
|
||||||
await Container.get(InternalHooks).onWorkflowPostExecute(execution.id, execution.workflowData, {
|
|
||||||
data: execution.data,
|
|
||||||
finished: false,
|
|
||||||
mode: execution.mode,
|
|
||||||
waitTill: execution.waitTill,
|
|
||||||
startedAt: execution.startedAt,
|
|
||||||
stoppedAt: execution.stoppedAt,
|
|
||||||
status: execution.status,
|
|
||||||
});
|
|
||||||
|
|
||||||
this.eventService.emit('workflow-post-execute', {
|
this.eventService.emit('workflow-post-execute', {
|
||||||
workflowId: execution.workflowData.id,
|
workflow: execution.workflowData,
|
||||||
workflowName: execution.workflowData.name,
|
|
||||||
executionId: execution.id,
|
executionId: execution.id,
|
||||||
success: execution.status === 'success',
|
|
||||||
isManual: execution.mode === 'manual',
|
|
||||||
runData: execution,
|
runData: execution,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -8,10 +8,14 @@ import { License } from '@/License';
|
||||||
import { GlobalConfig } from '@n8n/config';
|
import { GlobalConfig } from '@n8n/config';
|
||||||
import { N8N_VERSION } from '@/constants';
|
import { N8N_VERSION } from '@/constants';
|
||||||
import { WorkflowRepository } from '@/databases/repositories/workflow.repository';
|
import { WorkflowRepository } from '@/databases/repositories/workflow.repository';
|
||||||
|
import type { ExecutionStatus, INodesGraphResult, ITelemetryTrackProperties } from 'n8n-workflow';
|
||||||
|
import { get as pslGet } from 'psl';
|
||||||
import { TelemetryHelpers } from 'n8n-workflow';
|
import { TelemetryHelpers } from 'n8n-workflow';
|
||||||
import { NodeTypes } from '@/NodeTypes';
|
import { NodeTypes } from '@/NodeTypes';
|
||||||
import { SharedWorkflowRepository } from '@/databases/repositories/sharedWorkflow.repository';
|
import { SharedWorkflowRepository } from '@/databases/repositories/sharedWorkflow.repository';
|
||||||
import { ProjectRelationRepository } from '@/databases/repositories/projectRelation.repository';
|
import { ProjectRelationRepository } from '@/databases/repositories/projectRelation.repository';
|
||||||
|
import type { IExecutionTrackProperties } from '@/Interfaces';
|
||||||
|
import { determineFinalExecutionStatus } from '@/executionLifecycleHooks/shared/sharedHookFunctions';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export class TelemetryEventRelay {
|
export class TelemetryEventRelay {
|
||||||
|
@ -118,6 +122,9 @@ export class TelemetryEventRelay {
|
||||||
this.eventService.on('workflow-saved', async (event) => {
|
this.eventService.on('workflow-saved', async (event) => {
|
||||||
await this.workflowSaved(event);
|
await this.workflowSaved(event);
|
||||||
});
|
});
|
||||||
|
this.eventService.on('workflow-post-execute', async (event) => {
|
||||||
|
await this.workflowPostExecute(event);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private teamProjectUpdated({ userId, role, members, projectId }: Event['team-project-updated']) {
|
private teamProjectUpdated({ userId, role, members, projectId }: Event['team-project-updated']) {
|
||||||
|
@ -584,4 +591,138 @@ export class TelemetryEventRelay {
|
||||||
earliest_workflow_created: firstWorkflow?.createdAt,
|
earliest_workflow_created: firstWorkflow?.createdAt,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line complexity
|
||||||
|
private async workflowPostExecute({ workflow, runData, userId }: Event['workflow-post-execute']) {
|
||||||
|
if (!workflow.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (runData?.status === 'waiting') {
|
||||||
|
// No need to send telemetry or logs when the workflow hasn't finished yet.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const telemetryProperties: IExecutionTrackProperties = {
|
||||||
|
workflow_id: workflow.id,
|
||||||
|
is_manual: false,
|
||||||
|
version_cli: N8N_VERSION,
|
||||||
|
success: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (userId) {
|
||||||
|
telemetryProperties.user_id = userId;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (runData?.data.resultData.error?.message?.includes('canceled')) {
|
||||||
|
runData.status = 'canceled';
|
||||||
|
}
|
||||||
|
|
||||||
|
telemetryProperties.success = !!runData?.finished;
|
||||||
|
|
||||||
|
// const executionStatus: ExecutionStatus = runData?.status ?? 'unknown';
|
||||||
|
const executionStatus: ExecutionStatus = runData
|
||||||
|
? determineFinalExecutionStatus(runData)
|
||||||
|
: 'unknown';
|
||||||
|
|
||||||
|
if (runData !== undefined) {
|
||||||
|
telemetryProperties.execution_mode = runData.mode;
|
||||||
|
telemetryProperties.is_manual = runData.mode === 'manual';
|
||||||
|
|
||||||
|
let nodeGraphResult: INodesGraphResult | null = null;
|
||||||
|
|
||||||
|
if (!telemetryProperties.success && runData?.data.resultData.error) {
|
||||||
|
telemetryProperties.error_message = runData?.data.resultData.error.message;
|
||||||
|
let errorNodeName =
|
||||||
|
'node' in runData?.data.resultData.error
|
||||||
|
? runData?.data.resultData.error.node?.name
|
||||||
|
: undefined;
|
||||||
|
telemetryProperties.error_node_type =
|
||||||
|
'node' in runData?.data.resultData.error
|
||||||
|
? runData?.data.resultData.error.node?.type
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
if (runData.data.resultData.lastNodeExecuted) {
|
||||||
|
const lastNode = TelemetryHelpers.getNodeTypeForName(
|
||||||
|
workflow,
|
||||||
|
runData.data.resultData.lastNodeExecuted,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (lastNode !== undefined) {
|
||||||
|
telemetryProperties.error_node_type = lastNode.type;
|
||||||
|
errorNodeName = lastNode.name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (telemetryProperties.is_manual) {
|
||||||
|
nodeGraphResult = TelemetryHelpers.generateNodesGraph(workflow, this.nodeTypes);
|
||||||
|
telemetryProperties.node_graph = nodeGraphResult.nodeGraph;
|
||||||
|
telemetryProperties.node_graph_string = JSON.stringify(nodeGraphResult.nodeGraph);
|
||||||
|
|
||||||
|
if (errorNodeName) {
|
||||||
|
telemetryProperties.error_node_id = nodeGraphResult.nameIndices[errorNodeName];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (telemetryProperties.is_manual) {
|
||||||
|
if (!nodeGraphResult) {
|
||||||
|
nodeGraphResult = TelemetryHelpers.generateNodesGraph(workflow, this.nodeTypes);
|
||||||
|
}
|
||||||
|
|
||||||
|
let userRole: 'owner' | 'sharee' | undefined = undefined;
|
||||||
|
if (userId) {
|
||||||
|
const role = await this.sharedWorkflowRepository.findSharingRole(userId, workflow.id);
|
||||||
|
if (role) {
|
||||||
|
userRole = role === 'workflow:owner' ? 'owner' : 'sharee';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const manualExecEventProperties: ITelemetryTrackProperties = {
|
||||||
|
user_id: userId,
|
||||||
|
workflow_id: workflow.id,
|
||||||
|
status: executionStatus,
|
||||||
|
executionStatus: runData?.status ?? 'unknown',
|
||||||
|
error_message: telemetryProperties.error_message as string,
|
||||||
|
error_node_type: telemetryProperties.error_node_type,
|
||||||
|
node_graph_string: telemetryProperties.node_graph_string as string,
|
||||||
|
error_node_id: telemetryProperties.error_node_id as string,
|
||||||
|
webhook_domain: null,
|
||||||
|
sharing_role: userRole,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!manualExecEventProperties.node_graph_string) {
|
||||||
|
nodeGraphResult = TelemetryHelpers.generateNodesGraph(workflow, this.nodeTypes);
|
||||||
|
manualExecEventProperties.node_graph_string = JSON.stringify(nodeGraphResult.nodeGraph);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (runData.data.startData?.destinationNode) {
|
||||||
|
const telemetryPayload = {
|
||||||
|
...manualExecEventProperties,
|
||||||
|
node_type: TelemetryHelpers.getNodeTypeForName(
|
||||||
|
workflow,
|
||||||
|
runData.data.startData?.destinationNode,
|
||||||
|
)?.type,
|
||||||
|
node_id: nodeGraphResult.nameIndices[runData.data.startData?.destinationNode],
|
||||||
|
};
|
||||||
|
|
||||||
|
this.telemetry.track('Manual node exec finished', telemetryPayload);
|
||||||
|
} else {
|
||||||
|
nodeGraphResult.webhookNodeNames.forEach((name: string) => {
|
||||||
|
const execJson = runData.data.resultData.runData[name]?.[0]?.data?.main?.[0]?.[0]
|
||||||
|
?.json as { headers?: { origin?: string } };
|
||||||
|
if (execJson?.headers?.origin && execJson.headers.origin !== '') {
|
||||||
|
manualExecEventProperties.webhook_domain = pslGet(
|
||||||
|
execJson.headers.origin.replace(/^https?:\/\//, ''),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.telemetry.track('Manual workflow exec finished', manualExecEventProperties);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.telemetry.trackWorkflowExecution(telemetryProperties);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue