Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

✨ Introduce telemetry #2099

Merged
merged 22 commits into from
Oct 19, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,8 @@ module.exports = {
'undefined',
],

'no-void': ['error', { 'allowAsStatement': true }],

// ----------------------------------
// @typescript-eslint
// ----------------------------------
Expand Down Expand Up @@ -250,6 +252,11 @@ module.exports = {
*/
'@typescript-eslint/no-misused-promises': ['error', { checksVoidReturn: false }],

/**
* https://github.com/typescript-eslint/typescript-eslint/blob/v4.30.0/packages/eslint-plugin/docs/rules/no-floating-promises.md
*/
'@typescript-eslint/no-floating-promises': ['error', { ignoreVoid: true }],

/**
* https://eslint.org/docs/1.0.0/rules/no-throw-literal
*/
Expand Down
14 changes: 12 additions & 2 deletions packages/cli/commands/start.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
GenericHelpers,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
IExecutionsCurrentSummary,
InternalHooksManager,
LoadNodesAndCredentials,
NodeTypes,
Server,
Expand Down Expand Up @@ -92,9 +93,12 @@ export class Start extends Command {
setTimeout(() => {
// In case that something goes wrong with shutdown we
// kill after max. 30 seconds no matter what
console.log(`process exited after 30s`);
process.exit(processExitCode);
}, 30000);

await InternalHooksManager.getInstance().onN8nStop();

const skipWebhookDeregistration = config.get(
'endpoints.skipWebhoooksDeregistrationOnShutdown',
) as boolean;
Expand Down Expand Up @@ -151,9 +155,15 @@ export class Start extends Command {
LoggerProxy.init(logger);
logger.info('Initializing n8n process');

// todo remove a few versions after release
logger.info(
'\nn8n now checks for new versions and security updates. You can turn this off using the environment variable N8N_VERSION_NOTIFICATIONS_ENABLED to "false"\nFor more information, please refer to https://docs.n8n.io/getting-started/installation/advanced/configuration.html\n',
'\n' +
'****************************************************\n' +
'* *\n' +
'* n8n now sends selected, anonymous telemetry. *\n' +
'* For more details (and how to opt out): *\n' +
'* https://docs.n8n.io/reference/telemetry.html *\n' +
'* *\n' +
'****************************************************\n',
);

// Start directly with the init of the database to improve startup time
Expand Down
40 changes: 40 additions & 0 deletions packages/cli/config/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -649,6 +649,46 @@ const config = convict({
env: 'N8N_VERSION_NOTIFICATIONS_INFO_URL',
},
},

deployment: {
type: {
format: String,
default: 'default',
env: 'N8N_DEPLOYMENT_TYPE',
},
},

personalization: {
enabled: {
doc: 'Whether personalization is enabled.',
format: Boolean,
default: true,
env: 'N8N_PERSONALIZATION_ENABLED',
},
},

diagnostics: {
enabled: {
doc: 'Whether diagnostic mode is enabled.',
format: Boolean,
default: true,
env: 'N8N_DIAGNOSTICS_ENABLED',
},
config: {
frontend: {
doc: 'Diagnostics config for frontend.',
format: String,
default: '1zPn9bgWPzlQc0p8Gj1uiK6DOTn;https://telemetry.n8n.io',
env: 'N8N_DIAGNOSTICS_CONFIG_FRONTEND',
},
backend: {
doc: 'Diagnostics config for backend.',
format: String,
default: '1zPn7YoGC3ZXE9zLeTKLuQCB4F6;https://telemetry.n8n.io/v1/batch',
env: 'N8N_DIAGNOSTICS_CONFIG_BACKEND',
},
},
},
});

// Overwrite default configuration with settings which got defined in
Expand Down
1 change: 1 addition & 0 deletions packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@
"dependencies": {
"@oclif/command": "^1.5.18",
"@oclif/errors": "^1.2.2",
"@rudderstack/rudder-sdk-node": "^1.0.2",
"@types/json-diff": "^0.5.1",
"@types/jsonwebtoken": "^8.5.2",
"basic-auth": "^2.0.1",
Expand Down
49 changes: 49 additions & 0 deletions packages/cli/src/Interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
IRunData,
IRunExecutionData,
ITaskData,
ITelemetrySettings,
IWorkflowBase as IWorkflowBaseWorkflow,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
IWorkflowCredentials,
Expand Down Expand Up @@ -281,6 +282,40 @@ export interface IExternalHooksClass {
run(hookName: string, hookParameters?: any[]): Promise<void>;
}

export interface IDiagnosticInfo {
versionCli: string;
databaseType: DatabaseType;
notificationsEnabled: boolean;
disableProductionWebhooksOnMainProcess: boolean;
basicAuthActive: boolean;
systemInfo: {
os: {
type?: string;
version?: string;
};
memory?: number;
cpus: {
count?: number;
model?: string;
speed?: number;
};
};
executionVariables: {
[key: string]: string | number | undefined;
};
deploymentType: string;
}

export interface IInternalHooksClass {
onN8nStop(): Promise<void>;
onServerStarted(diagnosticInfo: IDiagnosticInfo): Promise<void>;
onPersonalizationSurveySubmitted(answers: IPersonalizationSurveyAnswers): Promise<void>;
onWorkflowCreated(workflow: IWorkflowBase): Promise<void>;
onWorkflowDeleted(workflowId: string): Promise<void>;
onWorkflowSaved(workflow: IWorkflowBase): Promise<void>;
onWorkflowPostExecute(workflow: IWorkflowBase, runData?: IRun): Promise<void>;
}

export interface IN8nConfig {
database: IN8nConfigDatabase;
endpoints: IN8nConfigEndpoints;
Expand Down Expand Up @@ -357,6 +392,20 @@ export interface IN8nUISettings {
};
versionNotifications: IVersionNotificationSettings;
instanceId: string;
telemetry: ITelemetrySettings;
personalizationSurvey: IPersonalizationSurvey;
}

export interface IPersonalizationSurveyAnswers {
companySize: string | null;
codingSkill: string | null;
workArea: string | null;
otherWorkArea: string | null;
}

export interface IPersonalizationSurvey {
answers?: IPersonalizationSurveyAnswers;
shouldShow: boolean;
}

export interface IPackageVersions {
Expand Down
105 changes: 105 additions & 0 deletions packages/cli/src/InternalHooks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
/* eslint-disable import/no-cycle */
import { IDataObject, IRun, TelemetryHelpers } from 'n8n-workflow';
import {
IDiagnosticInfo,
IInternalHooksClass,
IPersonalizationSurveyAnswers,
IWorkflowBase,
} from '.';
import { Telemetry } from './telemetry';

export class InternalHooksClass implements IInternalHooksClass {
constructor(private telemetry: Telemetry) {}

async onServerStarted(diagnosticInfo: IDiagnosticInfo): Promise<void> {
const info = {
version_cli: diagnosticInfo.versionCli,
db_type: diagnosticInfo.databaseType,
n8n_version_notifications_enabled: diagnosticInfo.notificationsEnabled,
n8n_disable_production_main_process: diagnosticInfo.disableProductionWebhooksOnMainProcess,
n8n_basic_auth_active: diagnosticInfo.basicAuthActive,
system_info: diagnosticInfo.systemInfo,
execution_variables: diagnosticInfo.executionVariables,
n8n_deployment_type: diagnosticInfo.deploymentType,
};
await this.telemetry.identify(info);
await this.telemetry.track('Instance started', info);
}

async onPersonalizationSurveySubmitted(answers: IPersonalizationSurveyAnswers): Promise<void> {
await this.telemetry.track('User responded to personalization questions', {
company_size: answers.companySize,
coding_skill: answers.codingSkill,
work_area: answers.workArea,
other_work_area: answers.otherWorkArea,
});
}

async onWorkflowCreated(workflow: IWorkflowBase): Promise<void> {
await this.telemetry.track('User created workflow', {
workflow_id: workflow.id,
node_graph: TelemetryHelpers.generateNodesGraph(workflow).nodeGraph,
});
}

async onWorkflowDeleted(workflowId: string): Promise<void> {
await this.telemetry.track('User deleted workflow', {
workflow_id: workflowId,
});
}

async onWorkflowSaved(workflow: IWorkflowBase): Promise<void> {
await this.telemetry.track('User saved workflow', {
workflow_id: workflow.id,
node_graph: TelemetryHelpers.generateNodesGraph(workflow).nodeGraph,
});
}

async onWorkflowPostExecute(workflow: IWorkflowBase, runData?: IRun): Promise<void> {
const properties: IDataObject = {
workflow_id: workflow.id,
is_manual: false,
};

if (runData !== undefined) {
properties.execution_mode = runData.mode;
if (runData.mode === 'manual') {
properties.is_manual = true;
}

properties.success = !!runData.finished;

if (!properties.success && runData?.data.resultData.error) {
properties.error_message = runData?.data.resultData.error.message;
let errorNodeName = runData?.data.resultData.error.node?.name;
properties.error_node_type = runData?.data.resultData.error.node?.type;

if (runData.data.resultData.lastNodeExecuted) {
const lastNode = TelemetryHelpers.getNodeTypeForName(
workflow,
runData.data.resultData.lastNodeExecuted,
);

if (lastNode !== undefined) {
properties.error_node_type = lastNode.type;
errorNodeName = lastNode.name;
}
}

if (properties.is_manual) {
const nodeGraphResult = TelemetryHelpers.generateNodesGraph(workflow);
properties.node_graph = nodeGraphResult.nodeGraph;
if (errorNodeName) {
properties.error_node_id = nodeGraphResult.nameIndices[errorNodeName];
}
}
}
}

void this.telemetry.trackWorkflowExecution(properties);
}

async onN8nStop(): Promise<void> {
await this.telemetry.trackN8nStop();
}
}
23 changes: 23 additions & 0 deletions packages/cli/src/InternalHooksManager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/* eslint-disable import/no-cycle */
import { InternalHooksClass } from './InternalHooks';
import { Telemetry } from './telemetry';

export class InternalHooksManager {
private static internalHooksInstance: InternalHooksClass;

static getInstance(): InternalHooksClass {
if (this.internalHooksInstance) {
return this.internalHooksInstance;
}

throw new Error('InternalHooks not initialized');
}

static init(instanceId: string): InternalHooksClass {
if (!this.internalHooksInstance) {
this.internalHooksInstance = new InternalHooksClass(new Telemetry(instanceId));
}

return this.internalHooksInstance;
}
}
63 changes: 63 additions & 0 deletions packages/cli/src/PersonalizationSurvey.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { readFileSync, writeFile } from 'fs';
import { promisify } from 'util';
import { UserSettings } from 'n8n-core';

import * as config from '../config';
// eslint-disable-next-line import/no-cycle
import { Db, IPersonalizationSurvey, IPersonalizationSurveyAnswers } from '.';

const fsWriteFile = promisify(writeFile);

const PERSONALIZATION_SURVEY_FILENAME = 'personalizationSurvey.json';

function loadSurveyFromDisk(): IPersonalizationSurveyAnswers | undefined {
const userSettingsPath = UserSettings.getUserN8nFolderPath();
try {
const surveyFile = readFileSync(
`${userSettingsPath}/${PERSONALIZATION_SURVEY_FILENAME}`,
'utf-8',
);
return JSON.parse(surveyFile) as IPersonalizationSurveyAnswers;
} catch (error) {
return undefined;
}
}

export async function writeSurveyToDisk(
surveyAnswers: IPersonalizationSurveyAnswers,
): Promise<void> {
const userSettingsPath = UserSettings.getUserN8nFolderPath();
await fsWriteFile(
`${userSettingsPath}/${PERSONALIZATION_SURVEY_FILENAME}`,
JSON.stringify(surveyAnswers, null, '\t'),
);
}

export async function preparePersonalizationSurvey(): Promise<IPersonalizationSurvey> {
const survey: IPersonalizationSurvey = {
shouldShow: false,
};

survey.answers = loadSurveyFromDisk();

if (survey.answers) {
return survey;
}

const enabled =
(config.get('personalization.enabled') as boolean) &&
(config.get('diagnostics.enabled') as boolean);

if (!enabled) {
return survey;
}

const workflowsExist = !!(await Db.collections.Workflow?.findOne());

if (workflowsExist) {
return survey;
}

survey.shouldShow = true;
return survey;
}
4 changes: 2 additions & 2 deletions packages/cli/src/ResponseHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,13 +90,13 @@ export function sendSuccessResponse(
}
}

export function sendErrorResponse(res: Response, error: ResponseError) {
export function sendErrorResponse(res: Response, error: ResponseError, shouldLog = true) {
let httpStatusCode = 500;
if (error.httpStatusCode) {
httpStatusCode = error.httpStatusCode;
}

if (process.env.NODE_ENV !== 'production') {
if (process.env.NODE_ENV !== 'production' && shouldLog) {
console.error('ERROR RESPONSE');
console.error(error);
}
Expand Down
Loading