mirror of
https://github.com/n8n-io/n8n.git
synced 2024-12-25 04:34:06 -08:00
refactor: Telemetry updates (#3529)
* Init unit tests for telemetry * Update telemetry tests * Test Workflow execution errored event * Add new tracking logic in pulse * cleanup * interfaces * Add event_version for Workflow execution count event * add version_cli in all events * add user saved credentials event * update manual wf exec finished, fixes * improve typings, lint * add node_graph_string in User clicked execute workflow button event * add User set node operation or mode event * Add instance started event in FE * Add User clicked retry execution button event * add expression editor event * add input node type to add node event * add User stopped workflow execution wvent * add error message in saved credential event * update stop execution event * add execution preflight event * Remove instance started even tfrom FE, add session started to FE,BE * improve typing * remove node_graph as property from all events * move back from default export * move psl npm package to cli package * cr * update webhook node domain logic * fix is_valid for User saved credentials event * fix Expression Editor variable selector event * add caused_by_credential in preflight event * undo webhook_domain * change node_type to full type * add webhook_domain property in manual execution event (#3680) * add webhook_domain property in manual execution event * lint fix
This commit is contained in:
parent
32c68eb126
commit
6b2db8e4f4
16
package-lock.json
generated
16
package-lock.json
generated
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"name": "n8n",
|
||||
"version": "0.184.0",
|
||||
"version": "0.185.0",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "n8n",
|
||||
"version": "0.184.0",
|
||||
"version": "0.185.0",
|
||||
"dependencies": {
|
||||
"@apidevtools/swagger-cli": "4.0.0",
|
||||
"@babel/core": "^7.14.6",
|
||||
|
@ -75,6 +75,7 @@
|
|||
"@types/parseurl": "^1.3.1",
|
||||
"@types/passport-jwt": "^3.0.6",
|
||||
"@types/promise-ftp": "^1.3.4",
|
||||
"@types/psl": "^1.1.0",
|
||||
"@types/quill": "^2.0.1",
|
||||
"@types/redis": "^2.8.11",
|
||||
"@types/request-promise-native": "~1.0.15",
|
||||
|
@ -222,6 +223,7 @@
|
|||
"prismjs": "^1.17.1",
|
||||
"prom-client": "^13.1.0",
|
||||
"promise-ftp": "^1.3.5",
|
||||
"psl": "^1.8.0",
|
||||
"qs": "^6.10.1",
|
||||
"quill": "^2.0.0-dev.3",
|
||||
"quill-autoformat": "^0.1.1",
|
||||
|
@ -15310,6 +15312,11 @@
|
|||
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz",
|
||||
"integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w=="
|
||||
},
|
||||
"node_modules/@types/psl": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/psl/-/psl-1.1.0.tgz",
|
||||
"integrity": "sha512-HhZnoLAvI2koev3czVPzBNRYvdrzJGLjQbWZhqFmS9Q6a0yumc5qtfSahBGb5g+6qWvA8iiQktqGkwoIXa/BNQ=="
|
||||
},
|
||||
"node_modules/@types/q": {
|
||||
"version": "1.5.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/q/-/q-1.5.5.tgz",
|
||||
|
@ -73705,6 +73712,11 @@
|
|||
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz",
|
||||
"integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w=="
|
||||
},
|
||||
"@types/psl": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/psl/-/psl-1.1.0.tgz",
|
||||
"integrity": "sha512-HhZnoLAvI2koev3czVPzBNRYvdrzJGLjQbWZhqFmS9Q6a0yumc5qtfSahBGb5g+6qWvA8iiQktqGkwoIXa/BNQ=="
|
||||
},
|
||||
"@types/q": {
|
||||
"version": "1.5.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/q/-/q-1.5.5.tgz",
|
||||
|
|
|
@ -75,6 +75,7 @@
|
|||
"@types/open": "^6.1.0",
|
||||
"@types/parseurl": "^1.3.1",
|
||||
"@types/passport-jwt": "^3.0.6",
|
||||
"@types/psl": "^1.1.0",
|
||||
"@types/request-promise-native": "~1.0.15",
|
||||
"@types/superagent": "4.1.13",
|
||||
"@types/supertest": "^2.0.11",
|
||||
|
@ -145,6 +146,7 @@
|
|||
"passport-jwt": "^4.0.0",
|
||||
"pg": "^8.3.0",
|
||||
"prom-client": "^13.1.0",
|
||||
"psl": "^1.8.0",
|
||||
"request-promise-native": "^1.0.7",
|
||||
"shelljs": "^0.8.5",
|
||||
"sqlite3": "^5.0.2",
|
||||
|
|
|
@ -13,6 +13,7 @@ import {
|
|||
IRunExecutionData,
|
||||
ITaskData,
|
||||
ITelemetrySettings,
|
||||
ITelemetryTrackProperties,
|
||||
IWorkflowBase as IWorkflowBaseWorkflow,
|
||||
Workflow,
|
||||
WorkflowExecuteMode,
|
||||
|
@ -667,3 +668,14 @@ export interface IWorkflowExecuteProcess {
|
|||
}
|
||||
|
||||
export type WhereClause = Record<string, { id: string }>;
|
||||
|
||||
// ----------------------------------
|
||||
// telemetry
|
||||
// ----------------------------------
|
||||
|
||||
export interface IExecutionTrackProperties extends ITelemetryTrackProperties {
|
||||
workflow_id: string;
|
||||
success: boolean;
|
||||
error_node_type?: string;
|
||||
is_manual: boolean;
|
||||
}
|
||||
|
|
|
@ -1,6 +1,13 @@
|
|||
/* eslint-disable import/no-cycle */
|
||||
import { get as pslGet } from 'psl';
|
||||
import { BinaryDataManager } from 'n8n-core';
|
||||
import { IDataObject, INodeTypes, IRun, TelemetryHelpers } from 'n8n-workflow';
|
||||
import {
|
||||
INodesGraphResult,
|
||||
INodeTypes,
|
||||
IRun,
|
||||
ITelemetryTrackProperties,
|
||||
TelemetryHelpers,
|
||||
} from 'n8n-workflow';
|
||||
import { snakeCase } from 'change-case';
|
||||
import {
|
||||
IDiagnosticInfo,
|
||||
|
@ -10,6 +17,7 @@ import {
|
|||
IWorkflowDb,
|
||||
} from '.';
|
||||
import { Telemetry } from './telemetry';
|
||||
import { IExecutionTrackProperties } from './Interfaces';
|
||||
|
||||
export class InternalHooksClass implements IInternalHooksClass {
|
||||
private versionCli: string;
|
||||
|
@ -48,6 +56,10 @@ export class InternalHooksClass implements IInternalHooksClass {
|
|||
]);
|
||||
}
|
||||
|
||||
async onFrontendSettingsAPI(sessionId?: string): Promise<void> {
|
||||
return this.telemetry.track('Session started', { session_id: sessionId });
|
||||
}
|
||||
|
||||
async onPersonalizationSurveySubmitted(
|
||||
userId: string,
|
||||
answers: Record<string, string>,
|
||||
|
@ -73,7 +85,6 @@ export class InternalHooksClass implements IInternalHooksClass {
|
|||
return this.telemetry.track('User created workflow', {
|
||||
user_id: userId,
|
||||
workflow_id: workflow.id,
|
||||
node_graph: nodeGraph,
|
||||
node_graph_string: JSON.stringify(nodeGraph),
|
||||
public_api: publicApi,
|
||||
});
|
||||
|
@ -98,7 +109,6 @@ export class InternalHooksClass implements IInternalHooksClass {
|
|||
return this.telemetry.track('User saved workflow', {
|
||||
user_id: userId,
|
||||
workflow_id: workflow.id,
|
||||
node_graph: nodeGraph,
|
||||
node_graph_string: JSON.stringify(nodeGraph),
|
||||
notes_count_overlapping: overlappingCount,
|
||||
notes_count_non_overlapping: notesCount - overlappingCount,
|
||||
|
@ -115,10 +125,16 @@ export class InternalHooksClass implements IInternalHooksClass {
|
|||
userId?: string,
|
||||
): Promise<void> {
|
||||
const promises = [Promise.resolve()];
|
||||
const properties: IDataObject = {
|
||||
workflow_id: workflow.id,
|
||||
|
||||
if (!workflow.id) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
const properties: IExecutionTrackProperties = {
|
||||
workflow_id: workflow.id.toString(),
|
||||
is_manual: false,
|
||||
version_cli: this.versionCli,
|
||||
success: false,
|
||||
};
|
||||
|
||||
if (userId) {
|
||||
|
@ -130,7 +146,7 @@ export class InternalHooksClass implements IInternalHooksClass {
|
|||
properties.success = !!runData.finished;
|
||||
properties.is_manual = runData.mode === 'manual';
|
||||
|
||||
let nodeGraphResult;
|
||||
let nodeGraphResult: INodesGraphResult | null = null;
|
||||
|
||||
if (!properties.success && runData?.data.resultData.error) {
|
||||
properties.error_message = runData?.data.resultData.error.message;
|
||||
|
@ -165,22 +181,19 @@ export class InternalHooksClass implements IInternalHooksClass {
|
|||
nodeGraphResult = TelemetryHelpers.generateNodesGraph(workflow, this.nodeTypes);
|
||||
}
|
||||
|
||||
const manualExecEventProperties = {
|
||||
workflow_id: workflow.id,
|
||||
const manualExecEventProperties: ITelemetryTrackProperties = {
|
||||
workflow_id: workflow.id.toString(),
|
||||
status: properties.success ? 'success' : 'failed',
|
||||
error_message: properties.error_message,
|
||||
error_message: properties.error_message as string,
|
||||
error_node_type: properties.error_node_type,
|
||||
node_graph: properties.node_graph,
|
||||
node_graph_string: properties.node_graph_string,
|
||||
error_node_id: properties.error_node_id,
|
||||
node_graph_string: properties.node_graph_string as string,
|
||||
error_node_id: properties.error_node_id as string,
|
||||
webhook_domain: null,
|
||||
};
|
||||
|
||||
if (!manualExecEventProperties.node_graph) {
|
||||
if (!manualExecEventProperties.node_graph_string) {
|
||||
nodeGraphResult = TelemetryHelpers.generateNodesGraph(workflow, this.nodeTypes);
|
||||
manualExecEventProperties.node_graph = nodeGraphResult.nodeGraph;
|
||||
manualExecEventProperties.node_graph_string = JSON.stringify(
|
||||
manualExecEventProperties.node_graph,
|
||||
);
|
||||
manualExecEventProperties.node_graph_string = JSON.stringify(nodeGraphResult.nodeGraph);
|
||||
}
|
||||
|
||||
if (runData.data.startData?.destinationNode) {
|
||||
|
@ -195,6 +208,16 @@ export class InternalHooksClass implements IInternalHooksClass {
|
|||
}),
|
||||
);
|
||||
} 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?:\/\//, ''),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
promises.push(
|
||||
this.telemetry.track('Manual workflow exec finished', manualExecEventProperties),
|
||||
);
|
||||
|
|
|
@ -2856,6 +2856,10 @@ class App {
|
|||
`/${this.restEndpoint}/settings`,
|
||||
ResponseHelper.send(
|
||||
async (req: express.Request, res: express.Response): Promise<IN8nUISettings> => {
|
||||
void InternalHooksManager.getInstance().onFrontendSettingsAPI(
|
||||
req.headers.sessionid as string,
|
||||
);
|
||||
|
||||
return this.getSettingsForFrontend();
|
||||
},
|
||||
),
|
||||
|
|
|
@ -2,37 +2,25 @@
|
|||
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||
import TelemetryClient from '@rudderstack/rudder-sdk-node';
|
||||
import { IDataObject, LoggerProxy } from 'n8n-workflow';
|
||||
import { ITelemetryTrackProperties, LoggerProxy } from 'n8n-workflow';
|
||||
import * as config from '../../config';
|
||||
import { IExecutionTrackProperties } from '../Interfaces';
|
||||
import { getLogger } from '../Logger';
|
||||
|
||||
type CountBufferItemKey =
|
||||
| 'manual_success_count'
|
||||
| 'manual_error_count'
|
||||
| 'prod_success_count'
|
||||
| 'prod_error_count';
|
||||
type ExecutionTrackDataKey = 'manual_error' | 'manual_success' | 'prod_error' | 'prod_success';
|
||||
|
||||
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;
|
||||
interface IExecutionTrackData {
|
||||
count: number;
|
||||
first: Date;
|
||||
}
|
||||
|
||||
type IFirstExecutions = {
|
||||
[key in FirstExecutionItemKey]: Date | undefined;
|
||||
};
|
||||
|
||||
interface IExecutionsBuffer {
|
||||
counts: IExecutionCountsBuffer;
|
||||
firstExecutions: IFirstExecutions;
|
||||
[workflowId: string]: {
|
||||
manual_error?: IExecutionTrackData;
|
||||
manual_success?: IExecutionTrackData;
|
||||
prod_error?: IExecutionTrackData;
|
||||
prod_success?: IExecutionTrackData;
|
||||
};
|
||||
}
|
||||
|
||||
export class Telemetry {
|
||||
|
@ -44,15 +32,7 @@ export class Telemetry {
|
|||
|
||||
private pulseIntervalReference: NodeJS.Timeout;
|
||||
|
||||
private executionCountsBuffer: IExecutionsBuffer = {
|
||||
counts: {},
|
||||
firstExecutions: {
|
||||
first_manual_error: undefined,
|
||||
first_manual_success: undefined,
|
||||
first_prod_error: undefined,
|
||||
first_prod_success: undefined,
|
||||
},
|
||||
};
|
||||
private executionCountsBuffer: IExecutionsBuffer = {};
|
||||
|
||||
constructor(instanceId: string, versionCli: string) {
|
||||
this.instanceId = instanceId;
|
||||
|
@ -71,85 +51,70 @@ export class Telemetry {
|
|||
return;
|
||||
}
|
||||
|
||||
this.client = new TelemetryClient(key, url, { logLevel });
|
||||
this.client = this.createTelemetryClient(key, url, logLevel);
|
||||
|
||||
this.startPulse();
|
||||
}
|
||||
}
|
||||
|
||||
private createTelemetryClient(
|
||||
key: string,
|
||||
url: string,
|
||||
logLevel: string,
|
||||
): TelemetryClient | undefined {
|
||||
return new TelemetryClient(key, url, { logLevel });
|
||||
}
|
||||
|
||||
private startPulse() {
|
||||
this.pulseIntervalReference = setInterval(async () => {
|
||||
void this.pulse();
|
||||
}, 6 * 60 * 60 * 1000); // every 6 hours
|
||||
}
|
||||
}
|
||||
|
||||
private async pulse(): Promise<unknown> {
|
||||
if (!this.client) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
const allPromises = Object.keys(this.executionCountsBuffer.counts).map(async (workflowId) => {
|
||||
const allPromises = Object.keys(this.executionCountsBuffer).map(async (workflowId) => {
|
||||
const promise = this.track('Workflow execution count', {
|
||||
version_cli: this.versionCli,
|
||||
event_version: '2',
|
||||
workflow_id: workflowId,
|
||||
...this.executionCountsBuffer.counts[workflowId],
|
||||
...this.executionCountsBuffer.firstExecutions,
|
||||
...this.executionCountsBuffer[workflowId],
|
||||
});
|
||||
|
||||
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', { version_cli: this.versionCli }));
|
||||
this.executionCountsBuffer = {};
|
||||
allPromises.push(this.track('pulse'));
|
||||
return Promise.all(allPromises);
|
||||
}
|
||||
|
||||
async trackWorkflowExecution(properties: IDataObject): Promise<void> {
|
||||
async trackWorkflowExecution(properties: IExecutionTrackProperties): Promise<void> {
|
||||
if (this.client) {
|
||||
const workflowId = properties.workflow_id as string;
|
||||
this.executionCountsBuffer.counts[workflowId] = this.executionCountsBuffer.counts[
|
||||
workflowId
|
||||
] ?? {
|
||||
manual_error_count: 0,
|
||||
manual_success_count: 0,
|
||||
prod_error_count: 0,
|
||||
prod_success_count: 0,
|
||||
const execTime = new Date();
|
||||
const workflowId = properties.workflow_id;
|
||||
|
||||
this.executionCountsBuffer[workflowId] = this.executionCountsBuffer[workflowId] ?? {};
|
||||
|
||||
const key: ExecutionTrackDataKey = `${properties.is_manual ? 'manual' : 'prod'}_${
|
||||
properties.success ? 'success' : 'error'
|
||||
}`;
|
||||
|
||||
if (!this.executionCountsBuffer[workflowId][key]) {
|
||||
this.executionCountsBuffer[workflowId][key] = {
|
||||
count: 1,
|
||||
first: execTime,
|
||||
};
|
||||
} else {
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
this.executionCountsBuffer[workflowId][key]!.count++;
|
||||
}
|
||||
|
||||
let countKey: CountBufferItemKey;
|
||||
let firstExecKey: FirstExecutionItemKey;
|
||||
|
||||
if (
|
||||
properties.success === false &&
|
||||
properties.error_node_type &&
|
||||
(properties.error_node_type as string).startsWith('n8n-nodes-base')
|
||||
) {
|
||||
// errored exec
|
||||
if (!properties.success && properties.error_node_type?.startsWith('n8n-nodes-base')) {
|
||||
void this.track('Workflow execution errored', properties);
|
||||
|
||||
if (properties.is_manual) {
|
||||
firstExecKey = 'first_manual_error';
|
||||
countKey = 'manual_error_count';
|
||||
} else {
|
||||
firstExecKey = 'first_prod_error';
|
||||
countKey = 'prod_error_count';
|
||||
}
|
||||
} else if (properties.is_manual) {
|
||||
countKey = 'manual_success_count';
|
||||
firstExecKey = 'first_manual_success';
|
||||
} else {
|
||||
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]++;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -165,7 +130,9 @@ export class Telemetry {
|
|||
});
|
||||
}
|
||||
|
||||
async identify(traits?: IDataObject): Promise<void> {
|
||||
async identify(traits?: {
|
||||
[key: string]: string | number | boolean | object | undefined | null;
|
||||
}): Promise<void> {
|
||||
return new Promise<void>((resolve) => {
|
||||
if (this.client) {
|
||||
this.client.identify(
|
||||
|
@ -185,20 +152,22 @@ export class Telemetry {
|
|||
});
|
||||
}
|
||||
|
||||
async track(
|
||||
eventName: string,
|
||||
properties: { [key: string]: unknown; user_id?: string } = {},
|
||||
): Promise<void> {
|
||||
async track(eventName: string, properties: ITelemetryTrackProperties = {}): Promise<void> {
|
||||
return new Promise<void>((resolve) => {
|
||||
if (this.client) {
|
||||
const { user_id } = properties;
|
||||
Object.assign(properties, { instance_id: this.instanceId });
|
||||
const updatedProperties: ITelemetryTrackProperties = {
|
||||
...properties,
|
||||
instance_id: this.instanceId,
|
||||
version_cli: this.versionCli,
|
||||
};
|
||||
|
||||
this.client.track(
|
||||
{
|
||||
userId: `${this.instanceId}${user_id ? `#${user_id}` : ''}`,
|
||||
anonymousId: '000000000000',
|
||||
event: eventName,
|
||||
properties,
|
||||
properties: updatedProperties,
|
||||
},
|
||||
resolve,
|
||||
);
|
||||
|
@ -207,4 +176,10 @@ export class Telemetry {
|
|||
}
|
||||
});
|
||||
}
|
||||
|
||||
// test helpers
|
||||
|
||||
getCountsBuffer(): IExecutionsBuffer {
|
||||
return this.executionCountsBuffer;
|
||||
}
|
||||
}
|
||||
|
|
381
packages/cli/test/unit/Telemetry.test.ts
Normal file
381
packages/cli/test/unit/Telemetry.test.ts
Normal file
|
@ -0,0 +1,381 @@
|
|||
import { Telemetry } from '../../src/telemetry';
|
||||
|
||||
jest.spyOn(Telemetry.prototype as any, 'createTelemetryClient').mockImplementation(() => {
|
||||
return {
|
||||
flush: () => {},
|
||||
identify: () => {},
|
||||
track: () => {},
|
||||
};
|
||||
});
|
||||
|
||||
describe('Telemetry', () => {
|
||||
let startPulseSpy: jest.SpyInstance;
|
||||
const spyTrack = jest.spyOn(Telemetry.prototype, 'track');
|
||||
|
||||
let telemetry: Telemetry;
|
||||
const n8nVersion = '0.0.0';
|
||||
const instanceId = 'Telemetry unit test';
|
||||
const testDateTime = new Date('2022-01-01 00:00:00');
|
||||
|
||||
beforeAll(() => {
|
||||
startPulseSpy = jest.spyOn(Telemetry.prototype as any, 'startPulse').mockImplementation(() => {});
|
||||
jest.useFakeTimers();
|
||||
jest.setSystemTime(testDateTime);
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
jest.clearAllTimers();
|
||||
jest.useRealTimers();
|
||||
startPulseSpy.mockRestore();
|
||||
telemetry.trackN8nStop();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
spyTrack.mockClear();
|
||||
telemetry = new Telemetry(instanceId, n8nVersion);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
telemetry.trackN8nStop();
|
||||
});
|
||||
|
||||
describe('trackN8nStop', () => {
|
||||
test('should call track method', () => {
|
||||
telemetry.trackN8nStop();
|
||||
expect(spyTrack).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('trackWorkflowExecution', () => {
|
||||
beforeEach(() => {
|
||||
jest.setSystemTime(testDateTime);
|
||||
});
|
||||
|
||||
test('should count executions correctly', async () => {
|
||||
const payload = {
|
||||
workflow_id: '1',
|
||||
is_manual: true,
|
||||
success: true,
|
||||
error_node_type: 'custom-nodes-base.node-type'
|
||||
};
|
||||
|
||||
payload.is_manual = true;
|
||||
payload.success = true;
|
||||
const execTime1 = fakeJestSystemTime('2022-01-01 12:00:00');
|
||||
await telemetry.trackWorkflowExecution(payload);
|
||||
fakeJestSystemTime('2022-01-01 12:30:00');
|
||||
await telemetry.trackWorkflowExecution(payload);
|
||||
|
||||
payload.is_manual = false;
|
||||
payload.success = true;
|
||||
const execTime2 = fakeJestSystemTime('2022-01-01 13:00:00');
|
||||
await telemetry.trackWorkflowExecution(payload);
|
||||
fakeJestSystemTime('2022-01-01 12:30:00');
|
||||
await telemetry.trackWorkflowExecution(payload);
|
||||
|
||||
payload.is_manual = true;
|
||||
payload.success = false;
|
||||
const execTime3 = fakeJestSystemTime('2022-01-01 14:00:00');
|
||||
await telemetry.trackWorkflowExecution(payload);
|
||||
fakeJestSystemTime('2022-01-01 12:30:00');
|
||||
await telemetry.trackWorkflowExecution(payload);
|
||||
|
||||
payload.is_manual = false;
|
||||
payload.success = false;
|
||||
const execTime4 = fakeJestSystemTime('2022-01-01 15:00:00');
|
||||
await telemetry.trackWorkflowExecution(payload);
|
||||
fakeJestSystemTime('2022-01-01 12:30:00');
|
||||
await telemetry.trackWorkflowExecution(payload);
|
||||
|
||||
expect(spyTrack).toHaveBeenCalledTimes(0);
|
||||
|
||||
const execBuffer = telemetry.getCountsBuffer();
|
||||
|
||||
expect(execBuffer['1'].manual_success?.count).toBe(2);
|
||||
expect(execBuffer['1'].manual_success?.first).toEqual(execTime1);
|
||||
expect(execBuffer['1'].prod_success?.count).toBe(2);
|
||||
expect(execBuffer['1'].prod_success?.first).toEqual(execTime2);
|
||||
expect(execBuffer['1'].manual_error?.count).toBe(2);
|
||||
expect(execBuffer['1'].manual_error?.first).toEqual(execTime3);
|
||||
expect(execBuffer['1'].prod_error?.count).toBe(2);
|
||||
expect(execBuffer['1'].prod_error?.first).toEqual(execTime4);
|
||||
});
|
||||
|
||||
test('should fire "Workflow execution errored" event for failed executions', async () => {
|
||||
const payload = {
|
||||
workflow_id: '1',
|
||||
is_manual: true,
|
||||
success: false,
|
||||
error_node_type: 'custom-nodes-base.node-type'
|
||||
};
|
||||
|
||||
const execTime1 = fakeJestSystemTime('2022-01-01 12:00:00');
|
||||
await telemetry.trackWorkflowExecution(payload);
|
||||
fakeJestSystemTime('2022-01-01 12:30:00');
|
||||
await telemetry.trackWorkflowExecution(payload);
|
||||
|
||||
let execBuffer = telemetry.getCountsBuffer();
|
||||
|
||||
// should not fire event for custom nodes
|
||||
expect(spyTrack).toHaveBeenCalledTimes(0);
|
||||
expect(execBuffer['1'].manual_error?.count).toBe(2);
|
||||
expect(execBuffer['1'].manual_error?.first).toEqual(execTime1);
|
||||
|
||||
payload.error_node_type = 'n8n-nodes-base.node-type';
|
||||
fakeJestSystemTime('2022-01-01 13:00:00');
|
||||
await telemetry.trackWorkflowExecution(payload);
|
||||
fakeJestSystemTime('2022-01-01 12:30:00');
|
||||
await telemetry.trackWorkflowExecution(payload);
|
||||
|
||||
execBuffer = telemetry.getCountsBuffer();
|
||||
|
||||
// should fire event for custom nodes
|
||||
expect(spyTrack).toHaveBeenCalledTimes(2);
|
||||
expect(spyTrack).toHaveBeenCalledWith('Workflow execution errored', payload);
|
||||
expect(execBuffer['1'].manual_error?.count).toBe(4);
|
||||
expect(execBuffer['1'].manual_error?.first).toEqual(execTime1);
|
||||
});
|
||||
|
||||
test('should track production executions count correctly', async () => {
|
||||
const payload = {
|
||||
workflow_id: '1',
|
||||
is_manual: false,
|
||||
success: true,
|
||||
error_node_type: 'node_type'
|
||||
};
|
||||
|
||||
// successful execution
|
||||
const execTime1 = fakeJestSystemTime('2022-01-01 12:00:00');
|
||||
await telemetry.trackWorkflowExecution(payload);
|
||||
|
||||
expect(spyTrack).toHaveBeenCalledTimes(0);
|
||||
|
||||
let execBuffer = telemetry.getCountsBuffer();
|
||||
expect(execBuffer['1'].manual_error).toBeUndefined();
|
||||
expect(execBuffer['1'].manual_success).toBeUndefined();
|
||||
expect(execBuffer['1'].prod_error).toBeUndefined();
|
||||
|
||||
expect(execBuffer['1'].prod_success?.count).toBe(1);
|
||||
expect(execBuffer['1'].prod_success?.first).toEqual(execTime1);
|
||||
|
||||
// successful execution n8n node
|
||||
payload.error_node_type = 'n8n-nodes-base.merge';
|
||||
payload.workflow_id = '2';
|
||||
|
||||
await telemetry.trackWorkflowExecution(payload);
|
||||
|
||||
expect(spyTrack).toHaveBeenCalledTimes(0);
|
||||
|
||||
execBuffer = telemetry.getCountsBuffer();
|
||||
expect(execBuffer['1'].manual_error).toBeUndefined();
|
||||
expect(execBuffer['1'].manual_success).toBeUndefined();
|
||||
expect(execBuffer['1'].prod_error).toBeUndefined();
|
||||
|
||||
expect(execBuffer['1'].prod_success?.count).toBe(1);
|
||||
expect(execBuffer['2'].prod_success?.count).toBe(1);
|
||||
|
||||
expect(execBuffer['1'].prod_success?.first).toEqual(execTime1);
|
||||
expect(execBuffer['2'].prod_success?.first).toEqual(execTime1);
|
||||
|
||||
// additional successful execution
|
||||
payload.error_node_type = 'n8n-nodes-base.merge';
|
||||
payload.workflow_id = '2';
|
||||
|
||||
await telemetry.trackWorkflowExecution(payload);
|
||||
|
||||
payload.error_node_type = 'n8n-nodes-base.merge';
|
||||
payload.workflow_id = '1';
|
||||
|
||||
await telemetry.trackWorkflowExecution(payload);
|
||||
|
||||
expect(spyTrack).toHaveBeenCalledTimes(0);
|
||||
|
||||
execBuffer = telemetry.getCountsBuffer();
|
||||
|
||||
expect(execBuffer['1'].manual_error).toBeUndefined();
|
||||
expect(execBuffer['1'].manual_success).toBeUndefined();
|
||||
expect(execBuffer['1'].prod_error).toBeUndefined();
|
||||
expect(execBuffer['2'].manual_error).toBeUndefined();
|
||||
expect(execBuffer['2'].manual_success).toBeUndefined();
|
||||
expect(execBuffer['2'].prod_error).toBeUndefined();
|
||||
|
||||
expect(execBuffer['1'].prod_success?.count).toBe(2);
|
||||
expect(execBuffer['2'].prod_success?.count).toBe(2);
|
||||
|
||||
expect(execBuffer['1'].prod_success?.first).toEqual(execTime1);
|
||||
expect(execBuffer['2'].prod_success?.first).toEqual(execTime1);
|
||||
|
||||
// failed execution
|
||||
const execTime2 = fakeJestSystemTime('2022-01-01 12:00:00');
|
||||
payload.error_node_type = 'custom-package.custom-node';
|
||||
payload.success = false;
|
||||
await telemetry.trackWorkflowExecution(payload);
|
||||
|
||||
expect(spyTrack).toHaveBeenCalledTimes(0);
|
||||
|
||||
execBuffer = telemetry.getCountsBuffer();
|
||||
|
||||
expect(execBuffer['1'].manual_error).toBeUndefined();
|
||||
expect(execBuffer['1'].manual_success).toBeUndefined();
|
||||
expect(execBuffer['2'].manual_error).toBeUndefined();
|
||||
expect(execBuffer['2'].manual_success).toBeUndefined();
|
||||
expect(execBuffer['2'].prod_error).toBeUndefined();
|
||||
|
||||
expect(execBuffer['1'].prod_error?.count).toBe(1);
|
||||
expect(execBuffer['1'].prod_success?.count).toBe(2);
|
||||
expect(execBuffer['2'].prod_success?.count).toBe(2);
|
||||
|
||||
expect(execBuffer['1'].prod_error?.first).toEqual(execTime2);
|
||||
expect(execBuffer['1'].prod_success?.first).toEqual(execTime1);
|
||||
expect(execBuffer['2'].prod_success?.first).toEqual(execTime1);
|
||||
|
||||
// failed execution n8n node
|
||||
payload.success = false;
|
||||
payload.error_node_type = 'n8n-nodes-base.merge';
|
||||
await telemetry.trackWorkflowExecution(payload);
|
||||
|
||||
expect(spyTrack).toHaveBeenCalledTimes(1);
|
||||
|
||||
execBuffer = telemetry.getCountsBuffer();
|
||||
|
||||
expect(execBuffer['1'].manual_error).toBeUndefined();
|
||||
expect(execBuffer['1'].manual_success).toBeUndefined();
|
||||
expect(execBuffer['2'].manual_error).toBeUndefined();
|
||||
expect(execBuffer['2'].manual_success).toBeUndefined();
|
||||
expect(execBuffer['2'].prod_error).toBeUndefined();
|
||||
expect(execBuffer['1'].prod_success?.count).toBe(2);
|
||||
expect(execBuffer['1'].prod_error?.count).toBe(2);
|
||||
expect(execBuffer['2'].prod_success?.count).toBe(2);
|
||||
|
||||
expect(execBuffer['1'].prod_error?.first).toEqual(execTime2);
|
||||
expect(execBuffer['1'].prod_success?.first).toEqual(execTime1);
|
||||
expect(execBuffer['2'].prod_success?.first).toEqual(execTime1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('pulse', () => {
|
||||
let pulseSpy: jest.SpyInstance;
|
||||
beforeAll(() => {
|
||||
startPulseSpy.mockRestore();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fakeJestSystemTime(testDateTime);
|
||||
pulseSpy = jest.spyOn(Telemetry.prototype as any, 'pulse');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
pulseSpy.mockClear();
|
||||
})
|
||||
|
||||
test('should trigger pulse in intervals', () => {
|
||||
expect(pulseSpy).toBeCalledTimes(0);
|
||||
|
||||
jest.advanceTimersToNextTimer();
|
||||
|
||||
expect(pulseSpy).toBeCalledTimes(1);
|
||||
expect(spyTrack).toHaveBeenCalledTimes(1);
|
||||
expect(spyTrack).toHaveBeenCalledWith('pulse');
|
||||
|
||||
jest.advanceTimersToNextTimer();
|
||||
|
||||
expect(pulseSpy).toBeCalledTimes(2);
|
||||
expect(spyTrack).toHaveBeenCalledTimes(2);
|
||||
expect(spyTrack).toHaveBeenCalledWith('pulse');
|
||||
});
|
||||
|
||||
test('should track workflow counts correctly', async () => {
|
||||
expect(pulseSpy).toBeCalledTimes(0);
|
||||
|
||||
let execBuffer = telemetry.getCountsBuffer();
|
||||
|
||||
// expect clear counters on start
|
||||
expect(Object.keys(execBuffer).length).toBe(0);
|
||||
|
||||
const payload = {
|
||||
workflow_id: '1',
|
||||
is_manual: true,
|
||||
success: true,
|
||||
error_node_type: 'custom-nodes-base.node-type'
|
||||
};
|
||||
|
||||
await telemetry.trackWorkflowExecution(payload);
|
||||
await telemetry.trackWorkflowExecution(payload);
|
||||
|
||||
payload.is_manual = false;
|
||||
payload.success = true;
|
||||
await telemetry.trackWorkflowExecution(payload);
|
||||
await telemetry.trackWorkflowExecution(payload);
|
||||
|
||||
payload.is_manual = true;
|
||||
payload.success = false;
|
||||
await telemetry.trackWorkflowExecution(payload);
|
||||
await telemetry.trackWorkflowExecution(payload);
|
||||
|
||||
payload.is_manual = false;
|
||||
payload.success = false;
|
||||
await telemetry.trackWorkflowExecution(payload);
|
||||
await telemetry.trackWorkflowExecution(payload);
|
||||
|
||||
payload.workflow_id = '2';
|
||||
await telemetry.trackWorkflowExecution(payload);
|
||||
await telemetry.trackWorkflowExecution(payload);
|
||||
|
||||
expect(spyTrack).toHaveBeenCalledTimes(0);
|
||||
expect(pulseSpy).toBeCalledTimes(0);
|
||||
|
||||
jest.advanceTimersToNextTimer();
|
||||
|
||||
execBuffer = telemetry.getCountsBuffer();
|
||||
|
||||
expect(pulseSpy).toBeCalledTimes(1);
|
||||
expect(spyTrack).toHaveBeenCalledTimes(3);
|
||||
console.log(spyTrack.getMockImplementation());
|
||||
expect(spyTrack).toHaveBeenNthCalledWith(1, 'Workflow execution count', {
|
||||
event_version: '2',
|
||||
workflow_id: '1',
|
||||
manual_error: {
|
||||
count: 2,
|
||||
first: testDateTime,
|
||||
},
|
||||
manual_success: {
|
||||
count: 2,
|
||||
first: testDateTime,
|
||||
},
|
||||
prod_error: {
|
||||
count: 2,
|
||||
first: testDateTime,
|
||||
},
|
||||
prod_success: {
|
||||
count: 2,
|
||||
first: testDateTime,
|
||||
}
|
||||
});
|
||||
expect(spyTrack).toHaveBeenNthCalledWith(2, 'Workflow execution count', {
|
||||
event_version: '2',
|
||||
workflow_id: '2',
|
||||
prod_error: {
|
||||
count: 2,
|
||||
first: testDateTime,
|
||||
}
|
||||
});
|
||||
expect(spyTrack).toHaveBeenNthCalledWith(3, 'pulse');
|
||||
expect(Object.keys(execBuffer).length).toBe(0);
|
||||
|
||||
jest.advanceTimersToNextTimer();
|
||||
|
||||
execBuffer = telemetry.getCountsBuffer();
|
||||
expect(Object.keys(execBuffer).length).toBe(0);
|
||||
|
||||
expect(pulseSpy).toBeCalledTimes(2);
|
||||
expect(spyTrack).toHaveBeenCalledTimes(4);
|
||||
expect(spyTrack).toHaveBeenNthCalledWith(4, 'pulse');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
const fakeJestSystemTime = (dateTime: string | Date): Date => {
|
||||
const dt = new Date(dateTime);
|
||||
jest.setSystemTime(dt);
|
||||
return dt;
|
||||
}
|
|
@ -112,6 +112,7 @@ import {
|
|||
INodeParameters,
|
||||
INodeProperties,
|
||||
INodeTypeDescription,
|
||||
ITelemetryTrackProperties,
|
||||
NodeHelpers,
|
||||
} from 'n8n-workflow';
|
||||
import CredentialIcon from '../CredentialIcon.vue';
|
||||
|
@ -620,7 +621,9 @@ export default mixins(showMessage, nodeHelpers).extend({
|
|||
|
||||
let credential;
|
||||
|
||||
if (this.mode === 'new' && !this.credentialId) {
|
||||
const isNewCredential = this.mode === 'new' && !this.credentialId;
|
||||
|
||||
if (isNewCredential) {
|
||||
credential = await this.createCredential(
|
||||
credentialDetails,
|
||||
);
|
||||
|
@ -647,6 +650,30 @@ export default mixins(showMessage, nodeHelpers).extend({
|
|||
this.authError = '';
|
||||
this.testedSuccessfully = false;
|
||||
}
|
||||
|
||||
const trackProperties: ITelemetryTrackProperties = {
|
||||
credential_type: credentialDetails.type,
|
||||
workflow_id: this.$store.getters.workflowId,
|
||||
credential_id: credential.id,
|
||||
is_complete: !!this.requiredPropertiesFilled,
|
||||
is_new: isNewCredential,
|
||||
};
|
||||
|
||||
if (this.isOAuthType) {
|
||||
trackProperties.is_valid = !!this.isOAuthConnected;
|
||||
} else if (this.isCredentialTestable) {
|
||||
trackProperties.is_valid = !!this.testedSuccessfully;
|
||||
}
|
||||
|
||||
if (this.$store.getters.activeNode) {
|
||||
trackProperties.node_type = this.$store.getters.activeNode.type;
|
||||
}
|
||||
|
||||
if (this.authError && this.authError !== '') {
|
||||
trackProperties.authError = this.authError;
|
||||
}
|
||||
|
||||
this.$telemetry.track('User saved credentials', trackProperties);
|
||||
}
|
||||
|
||||
return credential;
|
||||
|
|
|
@ -435,6 +435,12 @@ export default mixins(
|
|||
}
|
||||
|
||||
this.retryExecution(commandData.row, loadWorkflow);
|
||||
|
||||
this.$telemetry.track('User clicked retry execution button', {
|
||||
workflow_id: this.$store.getters.workflowId,
|
||||
execution_id: commandData.row.id,
|
||||
retry_type: loadWorkflow ? 'current' : 'original',
|
||||
});
|
||||
},
|
||||
getRowClass (data: IDataObject): string {
|
||||
const classes: string[] = [];
|
||||
|
|
|
@ -102,6 +102,59 @@ export default mixins(
|
|||
itemSelected (eventData: IVariableItemSelected) {
|
||||
(this.$refs.inputFieldExpression as any).itemSelected(eventData); // tslint:disable-line:no-any
|
||||
this.$externalHooks().run('expressionEdit.itemSelected', { parameter: this.parameter, value: this.value, selectedItem: eventData });
|
||||
|
||||
const trackProperties: {
|
||||
event_version: string;
|
||||
node_type_dest: string;
|
||||
node_type_source?: string;
|
||||
parameter_name_dest: string;
|
||||
parameter_name_source?: string;
|
||||
variable_type?: string;
|
||||
is_immediate_input: boolean;
|
||||
variable_expression: string;
|
||||
node_name: string;
|
||||
} = {
|
||||
event_version: '2',
|
||||
node_type_dest: this.$store.getters.activeNode.type,
|
||||
parameter_name_dest: this.parameter.displayName,
|
||||
is_immediate_input: false,
|
||||
variable_expression: eventData.variable,
|
||||
node_name: this.$store.getters.activeNode.name,
|
||||
};
|
||||
|
||||
if (eventData.variable) {
|
||||
let splitVar = eventData.variable.split('.');
|
||||
|
||||
if (eventData.variable.startsWith('Object.keys')) {
|
||||
splitVar = eventData.variable.split('(')[1].split(')')[0].split('.');
|
||||
trackProperties.variable_type = 'Keys';
|
||||
} else if (eventData.variable.startsWith('Object.values')) {
|
||||
splitVar = eventData.variable.split('(')[1].split(')')[0].split('.');
|
||||
trackProperties.variable_type = 'Values';
|
||||
} else {
|
||||
trackProperties.variable_type = 'Raw value';
|
||||
}
|
||||
|
||||
if (splitVar[0].startsWith('$node')) {
|
||||
const sourceNodeName = splitVar[0].split('"')[1];
|
||||
trackProperties.node_type_source = this.$store.getters.getNodeByName(sourceNodeName).type;
|
||||
const nodeConnections: Array<Array<{ node: string }>> = this.$store.getters.outgoingConnectionsByNodeName(sourceNodeName).main;
|
||||
trackProperties.is_immediate_input = (nodeConnections && nodeConnections[0] && !!nodeConnections[0].find(({ node }) => node === this.$store.getters.activeNode.name)) ? true : false;
|
||||
|
||||
if (splitVar[1].startsWith('parameter')) {
|
||||
trackProperties.parameter_name_source = splitVar[1].split('"')[1];
|
||||
}
|
||||
|
||||
} else {
|
||||
trackProperties.is_immediate_input = true;
|
||||
|
||||
if(splitVar[0].startsWith('$parameter')) {
|
||||
trackProperties.parameter_name_source = splitVar[0].split('"')[1];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.$telemetry.track('User inserted item from Expression Editor variable selector', trackProperties);
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
|
|
|
@ -853,6 +853,17 @@ export default mixins(
|
|||
};
|
||||
|
||||
this.$emit('valueChanged', parameterData);
|
||||
|
||||
if (this.parameter.name === 'operation' || this.parameter.name === 'mode') {
|
||||
this.$telemetry.track('User set node operation or mode', {
|
||||
workflow_id: this.$store.getters.workflowId,
|
||||
node_type: this.node && this.node.type,
|
||||
resource: this.node && this.node.parameters.resource,
|
||||
is_custom: value === CUSTOM_API_CALL_KEY,
|
||||
session_id: this.$store.getters['ui/ndvSessionId'],
|
||||
parameter: this.parameter.name,
|
||||
});
|
||||
}
|
||||
},
|
||||
optionSelected (command: string) {
|
||||
if (command === 'resetValue') {
|
||||
|
|
|
@ -258,11 +258,7 @@ export const workflowHelpers = mixins(
|
|||
return workflowIssues;
|
||||
},
|
||||
|
||||
// Returns a workflow instance.
|
||||
getWorkflow (nodes?: INodeUi[], connections?: IConnections, copyData?: boolean): Workflow {
|
||||
nodes = nodes || this.getNodes();
|
||||
connections = connections || (this.$store.getters.allConnections as IConnections);
|
||||
|
||||
getNodeTypes (): INodeTypes {
|
||||
const nodeTypes: INodeTypes = {
|
||||
nodeTypes: {},
|
||||
init: async (nodeTypes?: INodeTypeData): Promise<void> => { },
|
||||
|
@ -287,6 +283,15 @@ export const workflowHelpers = mixins(
|
|||
},
|
||||
};
|
||||
|
||||
return nodeTypes;
|
||||
},
|
||||
|
||||
// Returns a workflow instance.
|
||||
getWorkflow (nodes?: INodeUi[], connections?: IConnections, copyData?: boolean): Workflow {
|
||||
nodes = nodes || this.getNodes();
|
||||
connections = connections || (this.$store.getters.allConnections as IConnections);
|
||||
|
||||
const nodeTypes = this.getNodeTypes();
|
||||
let workflowId = this.$store.getters.workflowId;
|
||||
if (workflowId === PLACEHOLDER_EMPTY_WORKFLOW_ID) {
|
||||
workflowId = undefined;
|
||||
|
|
|
@ -7,7 +7,9 @@ import {
|
|||
import {
|
||||
IRunData,
|
||||
IRunExecutionData,
|
||||
IWorkflowBase,
|
||||
NodeHelpers,
|
||||
TelemetryHelpers,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import { externalHooks } from '@/components/mixins/externalHooks';
|
||||
|
@ -77,11 +79,32 @@ export const workflowRun = mixins(
|
|||
if (workflowIssues !== null) {
|
||||
const errorMessages = [];
|
||||
let nodeIssues: string[];
|
||||
const trackNodeIssues: Array<{
|
||||
node_type: string;
|
||||
error: string;
|
||||
}> = [];
|
||||
const trackErrorNodeTypes: string[] = [];
|
||||
for (const nodeName of Object.keys(workflowIssues)) {
|
||||
nodeIssues = NodeHelpers.nodeIssuesToString(workflowIssues[nodeName]);
|
||||
let issueNodeType = 'UNKNOWN';
|
||||
const issueNode = this.$store.getters.getNodeByName(nodeName);
|
||||
|
||||
if (issueNode) {
|
||||
issueNodeType = issueNode.type;
|
||||
}
|
||||
|
||||
trackErrorNodeTypes.push(issueNodeType);
|
||||
const trackNodeIssue = {
|
||||
node_type: issueNodeType,
|
||||
error: '',
|
||||
caused_by_credential: !!workflowIssues[nodeName].credentials,
|
||||
};
|
||||
|
||||
for (const nodeIssue of nodeIssues) {
|
||||
errorMessages.push(`${nodeName}: ${nodeIssue}`);
|
||||
trackNodeIssue.error = trackNodeIssue.error.concat(', ', nodeIssue);
|
||||
}
|
||||
trackNodeIssues.push(trackNodeIssue);
|
||||
}
|
||||
|
||||
this.$showMessage({
|
||||
|
@ -92,6 +115,17 @@ export const workflowRun = mixins(
|
|||
});
|
||||
this.$titleSet(workflow.name as string, 'ERROR');
|
||||
this.$externalHooks().run('workflowRun.runError', { errorMessages, nodeName });
|
||||
|
||||
this.getWorkflowDataToSave().then((workflowData) => {
|
||||
this.$telemetry.track('Workflow execution preflight failed', {
|
||||
workflow_id: workflow.id,
|
||||
workflow_name: workflow.name,
|
||||
execution_type: nodeName ? 'node' : 'workflow',
|
||||
node_graph_string: JSON.stringify(TelemetryHelpers.generateNodesGraph(workflowData as IWorkflowBase, this.getNodeTypes()).nodeGraph),
|
||||
error_node_types: JSON.stringify(trackErrorNodeTypes),
|
||||
errors: JSON.stringify(trackNodeIssues),
|
||||
});
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import _Vue from "vue";
|
||||
import {
|
||||
ITelemetrySettings,
|
||||
ITelemetryTrackProperties,
|
||||
IDataObject,
|
||||
} from 'n8n-workflow';
|
||||
import { ILogLevel, INodeCreateElement, IRootState } from "@/Interface";
|
||||
|
@ -72,6 +73,7 @@ class Telemetry {
|
|||
this.loadTelemetryLibrary(options.config.key, options.config.url, { integrations: { All: false }, loadIntegration: false, ...logging});
|
||||
this.identify(instanceId, userId);
|
||||
this.flushPageEvents();
|
||||
this.track('Session started', { session_id: store.getters.sessionId });
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -86,9 +88,14 @@ class Telemetry {
|
|||
}
|
||||
}
|
||||
|
||||
track(event: string, properties?: IDataObject) {
|
||||
track(event: string, properties?: ITelemetryTrackProperties) {
|
||||
if (this.telemetry) {
|
||||
this.telemetry.track(event, properties);
|
||||
const updatedProperties = {
|
||||
...properties,
|
||||
version_cli: this.store && this.store.getters.versionCli,
|
||||
};
|
||||
|
||||
this.telemetry.track(event, updatedProperties);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -131,21 +138,21 @@ class Telemetry {
|
|||
if (properties.createNodeActive !== false) {
|
||||
this.resetNodesPanelSession();
|
||||
properties.nodes_panel_session_id = this.userNodesPanelSession.sessionId;
|
||||
this.telemetry.track('User opened nodes panel', properties);
|
||||
this.track('User opened nodes panel', properties);
|
||||
}
|
||||
break;
|
||||
case 'nodeCreateList.selectedTypeChanged':
|
||||
this.userNodesPanelSession.data.filterMode = properties.new_filter as string;
|
||||
this.telemetry.track('User changed nodes panel filter', properties);
|
||||
this.track('User changed nodes panel filter', properties);
|
||||
break;
|
||||
case 'nodeCreateList.destroyed':
|
||||
if(this.userNodesPanelSession.data.nodeFilter.length > 0 && this.userNodesPanelSession.data.nodeFilter !== '') {
|
||||
this.telemetry.track('User entered nodes panel search term', this.generateNodesPanelEvent());
|
||||
this.track('User entered nodes panel search term', this.generateNodesPanelEvent());
|
||||
}
|
||||
break;
|
||||
case 'nodeCreateList.nodeFilterChanged':
|
||||
if((properties.newValue as string).length === 0 && this.userNodesPanelSession.data.nodeFilter.length > 0) {
|
||||
this.telemetry.track('User entered nodes panel search term', this.generateNodesPanelEvent());
|
||||
this.track('User entered nodes panel search term', this.generateNodesPanelEvent());
|
||||
}
|
||||
|
||||
if((properties.newValue as string).length > (properties.oldValue as string || '').length) {
|
||||
|
@ -155,7 +162,7 @@ class Telemetry {
|
|||
break;
|
||||
case 'nodeCreateList.onCategoryExpanded':
|
||||
properties.is_subcategory = false;
|
||||
this.telemetry.track('User viewed node category', properties);
|
||||
this.track('User viewed node category', properties);
|
||||
break;
|
||||
case 'nodeCreateList.onSubcategorySelected':
|
||||
const selectedProperties = (properties.selected as IDataObject).properties as IDataObject;
|
||||
|
@ -164,13 +171,13 @@ class Telemetry {
|
|||
}
|
||||
properties.is_subcategory = true;
|
||||
delete properties.selected;
|
||||
this.telemetry.track('User viewed node category', properties);
|
||||
this.track('User viewed node category', properties);
|
||||
break;
|
||||
case 'nodeView.addNodeButton':
|
||||
this.telemetry.track('User added node to workflow canvas', properties);
|
||||
this.track('User added node to workflow canvas', properties);
|
||||
break;
|
||||
case 'nodeView.addSticky':
|
||||
this.telemetry.track('User inserted workflow note', properties);
|
||||
this.track('User inserted workflow note', properties);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
|
|
|
@ -193,6 +193,9 @@ import {
|
|||
IRun,
|
||||
ITaskData,
|
||||
INodeCredentialsDetails,
|
||||
TelemetryHelpers,
|
||||
ITelemetryTrackProperties,
|
||||
IWorkflowBase,
|
||||
} from 'n8n-workflow';
|
||||
import {
|
||||
ICredentialsResponse,
|
||||
|
@ -409,7 +412,13 @@ export default mixins(
|
|||
this.runWorkflow(nodeName, source);
|
||||
},
|
||||
onRunWorkflow() {
|
||||
this.$telemetry.track('User clicked execute workflow button', { workflow_id: this.$store.getters.workflowId });
|
||||
this.getWorkflowDataToSave().then((workflowData) => {
|
||||
this.$telemetry.track('User clicked execute workflow button', {
|
||||
workflow_id: this.$store.getters.workflowId,
|
||||
node_graph_string: JSON.stringify(TelemetryHelpers.generateNodesGraph(workflowData as IWorkflowBase, this.getNodeTypes()).nodeGraph),
|
||||
});
|
||||
});
|
||||
|
||||
this.runWorkflow();
|
||||
},
|
||||
onCreateMenuHoverIn(mouseinEvent: MouseEvent) {
|
||||
|
@ -1169,6 +1178,15 @@ export default mixins(
|
|||
}
|
||||
}
|
||||
this.stopExecutionInProgress = false;
|
||||
|
||||
this.getWorkflowDataToSave().then((workflowData) => {
|
||||
const trackProps = {
|
||||
workflow_id: this.$store.getters.workflowId,
|
||||
node_graph_string: JSON.stringify(TelemetryHelpers.generateNodesGraph(workflowData as IWorkflowBase, this.getNodeTypes()).nodeGraph),
|
||||
};
|
||||
|
||||
this.$telemetry.track('User clicked stop workflow execution', trackProps);
|
||||
});
|
||||
},
|
||||
|
||||
async stopWaitingForWebhook () {
|
||||
|
@ -1501,11 +1519,17 @@ export default mixins(
|
|||
this.$telemetry.trackNodesPanel('nodeView.addSticky', { workflow_id: this.$store.getters.workflowId });
|
||||
} else {
|
||||
this.$externalHooks().run('nodeView.addNodeButton', { nodeTypeName });
|
||||
this.$telemetry.trackNodesPanel('nodeView.addNodeButton', {
|
||||
const trackProperties: ITelemetryTrackProperties = {
|
||||
node_type: nodeTypeName,
|
||||
workflow_id: this.$store.getters.workflowId,
|
||||
drag_and_drop: options.dragAndDrop,
|
||||
} as IDataObject);
|
||||
};
|
||||
|
||||
if (lastSelectedNode) {
|
||||
trackProperties.input_node_type = lastSelectedNode.type;
|
||||
}
|
||||
|
||||
this.$telemetry.trackNodesPanel('nodeView.addNodeButton', trackProperties);
|
||||
}
|
||||
|
||||
// Automatically deselect all nodes and select the current one and also active
|
||||
|
|
|
@ -52,13 +52,13 @@
|
|||
"typescript": "~4.6.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@n8n_io/riot-tmpl": "^1.0.1",
|
||||
"jmespath": "^0.16.0",
|
||||
"lodash.get": "^4.4.2",
|
||||
"lodash.isequal": "^4.5.0",
|
||||
"lodash.merge": "^4.6.2",
|
||||
"lodash.set": "^4.3.2",
|
||||
"luxon": "^2.3.0",
|
||||
"@n8n_io/riot-tmpl": "^1.0.1",
|
||||
"xml2js": "^0.4.23"
|
||||
},
|
||||
"jest": {
|
||||
|
|
|
@ -1476,6 +1476,11 @@ export type PropertiesOf<M extends { resource: string; operation: string }> = Ar
|
|||
|
||||
// Telemetry
|
||||
|
||||
export interface ITelemetryTrackProperties {
|
||||
user_id?: string;
|
||||
[key: string]: GenericValue;
|
||||
}
|
||||
|
||||
export interface INodesGraph {
|
||||
node_types: string[];
|
||||
node_connections: IDataObject[];
|
||||
|
@ -1519,6 +1524,7 @@ export interface INodeNameIndex {
|
|||
export interface INodesGraphResult {
|
||||
nodeGraph: INodesGraph;
|
||||
nameIndices: INodeNameIndex;
|
||||
webhookNodeNames: string[];
|
||||
}
|
||||
|
||||
export interface ITelemetryClientConfig {
|
||||
|
|
|
@ -11,8 +11,6 @@ import {
|
|||
} from '.';
|
||||
import { INodeType } from './Interfaces';
|
||||
|
||||
import { getInstance as getLoggerInstance } from './LoggerProxy';
|
||||
|
||||
const STICKY_NODE_TYPE = 'n8n-nodes-base.stickyNote';
|
||||
|
||||
export function getNodeTypeForName(workflow: IWorkflowBase, nodeName: string): INode | undefined {
|
||||
|
@ -124,6 +122,7 @@ export function generateNodesGraph(
|
|||
notes: {},
|
||||
};
|
||||
const nodeNameAndIndex: INodeNameIndex = {};
|
||||
const webhookNodeNames: string[] = [];
|
||||
|
||||
try {
|
||||
const notes = workflow.nodes.filter((node) => node.type === STICKY_NODE_TYPE);
|
||||
|
@ -177,6 +176,8 @@ export function generateNodesGraph(
|
|||
nodeItem.domain_base = getDomainBase(url);
|
||||
nodeItem.domain_path = getDomainPath(url);
|
||||
nodeItem.method = node.parameters.requestMethod as string;
|
||||
} else if (node.type === 'n8n-nodes-base.webhook') {
|
||||
webhookNodeNames.push(node.name);
|
||||
} else {
|
||||
const nodeType = nodeTypes.getByNameAndVersion(node.type);
|
||||
|
||||
|
@ -210,12 +211,9 @@ export function generateNodesGraph(
|
|||
});
|
||||
});
|
||||
});
|
||||
} catch (e) {
|
||||
const logger = getLoggerInstance();
|
||||
logger.warn(`Failed to generate nodes graph for workflowId: ${workflow.id as string | number}`);
|
||||
logger.warn((e as Error).message);
|
||||
logger.warn((e as Error).stack ?? '');
|
||||
} catch (_) {
|
||||
return { nodeGraph: nodesGraph, nameIndices: nodeNameAndIndex, webhookNodeNames };
|
||||
}
|
||||
|
||||
return { nodeGraph: nodesGraph, nameIndices: nodeNameAndIndex };
|
||||
return { nodeGraph: nodesGraph, nameIndices: nodeNameAndIndex, webhookNodeNames };
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue