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

Implement telemetry events #2868

Merged
Show file tree
Hide file tree
Changes from 15 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
35 changes: 31 additions & 4 deletions packages/cli/src/Interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -333,6 +333,16 @@ export interface IDiagnosticInfo {
};
deploymentType: string;
binaryDataMode: string;
n8n_multi_user_allowed: boolean;
smtp_set_up: boolean;
}

export interface ITelemetryUserDeletionData {
user_id: string;
target_user_old_status: 'active' | 'invited';
migration_strategy?: 'transfer_data' | 'delete_data';
target_user_id?: string;
migration_user_id?: string;
}

export interface IInternalHooksClass {
Expand All @@ -341,15 +351,32 @@ export interface IInternalHooksClass {
diagnosticInfo: IDiagnosticInfo,
firstWorkflowCreatedAt?: Date,
): Promise<unknown[]>;
onPersonalizationSurveySubmitted(answers: IPersonalizationSurveyAnswers): Promise<void>;
onWorkflowCreated(workflow: IWorkflowBase): Promise<void>;
onWorkflowDeleted(workflowId: string): Promise<void>;
onWorkflowSaved(workflow: IWorkflowBase): Promise<void>;
onPersonalizationSurveySubmitted(
userId: string,
answers: IPersonalizationSurveyAnswers,
): Promise<void>;
onWorkflowCreated(userId: string, workflow: IWorkflowBase): Promise<void>;
onWorkflowDeleted(userId: string, workflowId: string): Promise<void>;
onWorkflowSaved(userId: string, workflow: IWorkflowBase): Promise<void>;
onWorkflowPostExecute(
executionId: string,
workflow: IWorkflowBase,
runData?: IRun,
userId?: string,
): Promise<void>;
onUserDeletion(userId: string, userDeletionData: ITelemetryUserDeletionData): Promise<void>;
onUserInvite(userInviteData: { user_id: string; target_user_id: string[] }): Promise<void>;
onUserReinvite(userReinviteData: { user_id: string; target_user_id: string }): Promise<void>;
onUserUpdate(userUpdateData: { user_id: string; fields_changed: string[] }): Promise<void>;
onUserInviteEmailClick(userInviteClickData: { user_id: string }): Promise<void>;
onUserPasswordResetEmailClick(userPasswordResetData: { user_id: string }): Promise<void>;
onUserTransactionalEmail(userTransactionalEmailData: {
user_id: string;
message_type: 'Reset password' | 'New user invite' | 'Resend invite';
}): Promise<void>;
onUserPasswordResetRequestClick(userPasswordResetData: { user_id: string }): Promise<void>;
onInstanceOwnerSetup(instanceOwnerSetupData: { user_id: string }): Promise<void>;
onUserSignup(userSignupData: { user_id: string }): Promise<void>;
}

export interface IN8nConfig {
Expand Down
81 changes: 77 additions & 4 deletions packages/cli/src/InternalHooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
IDiagnosticInfo,
IInternalHooksClass,
IPersonalizationSurveyAnswers,
ITelemetryUserDeletionData,
IWorkflowBase,
IWorkflowDb,
} from '.';
Expand Down Expand Up @@ -34,6 +35,8 @@ export class InternalHooksClass implements IInternalHooksClass {
execution_variables: diagnosticInfo.executionVariables,
n8n_deployment_type: diagnosticInfo.deploymentType,
n8n_binary_data_mode: diagnosticInfo.binaryDataMode,
n8n_multi_user_allowed: diagnosticInfo.n8n_multi_user_allowed,
smtp_set_up: diagnosticInfo.smtp_set_up,
};

return Promise.all([
Expand All @@ -45,8 +48,12 @@ export class InternalHooksClass implements IInternalHooksClass {
]);
}

async onPersonalizationSurveySubmitted(answers: IPersonalizationSurveyAnswers): Promise<void> {
async onPersonalizationSurveySubmitted(
userId: string,
answers: IPersonalizationSurveyAnswers,
): Promise<void> {
return this.telemetry.track('User responded to personalization questions', {
user_id: userId,
company_size: answers.companySize,
coding_skill: answers.codingSkill,
work_area: answers.workArea,
Expand All @@ -56,25 +63,28 @@ export class InternalHooksClass implements IInternalHooksClass {
});
}

async onWorkflowCreated(workflow: IWorkflowBase): Promise<void> {
async onWorkflowCreated(userId: string, workflow: IWorkflowBase): Promise<void> {
const { nodeGraph } = TelemetryHelpers.generateNodesGraph(workflow, this.nodeTypes);
return this.telemetry.track('User created workflow', {
user_id: userId,
workflow_id: workflow.id,
node_graph: nodeGraph,
node_graph_string: JSON.stringify(nodeGraph),
});
}

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

async onWorkflowSaved(workflow: IWorkflowDb): Promise<void> {
async onWorkflowSaved(userId: string, workflow: IWorkflowDb): Promise<void> {
const { nodeGraph } = TelemetryHelpers.generateNodesGraph(workflow, this.nodeTypes);

return this.telemetry.track('User saved workflow', {
user_id: userId,
workflow_id: workflow.id,
node_graph: nodeGraph,
node_graph_string: JSON.stringify(nodeGraph),
Expand All @@ -87,6 +97,7 @@ export class InternalHooksClass implements IInternalHooksClass {
executionId: string,
workflow: IWorkflowBase,
runData?: IRun,
userId?: string,
): Promise<void> {
const promises = [Promise.resolve()];
const properties: IDataObject = {
Expand All @@ -95,6 +106,10 @@ export class InternalHooksClass implements IInternalHooksClass {
version_cli: this.versionCli,
};

if (userId) {
properties.user_id = userId;
}

if (runData !== undefined) {
properties.execution_mode = runData.mode;
properties.success = !!runData.finished;
Expand Down Expand Up @@ -188,4 +203,62 @@ export class InternalHooksClass implements IInternalHooksClass {

return Promise.race([timeoutPromise, this.telemetry.trackN8nStop()]);
}

async onUserDeletion(
userId: string,
userDeletionData: ITelemetryUserDeletionData,
): Promise<void> {
return this.telemetry.track('User deleted user', { ...userDeletionData, user_id: userId });
}

async onUserInvite(userInviteData: { user_id: string; target_user_id: string[] }): Promise<void> {
return this.telemetry.track('User invited new user', userInviteData);
}

async onUserReinvite(userReinviteData: {
user_id: string;
target_user_id: string;
}): Promise<void> {
return this.telemetry.track('User resent new user invite email', userReinviteData);
}

async onUserUpdate(userUpdateData: { user_id: string; fields_changed: string[] }): Promise<void> {
return this.telemetry.track('User changed personal settings', userUpdateData);
}

async onUserInviteEmailClick(userInviteClickData: { user_id: string }): Promise<void> {
return this.telemetry.track('User clicked invite link from email', userInviteClickData);
}

async onUserPasswordResetEmailClick(userPasswordResetData: { user_id: string }): Promise<void> {
return this.telemetry.track(
'User clicked password reset link from email',
userPasswordResetData,
);
}

async onUserTransactionalEmail(userTransactionalEmailData: {
user_id: string;
message_type: 'Reset password' | 'New user invite' | 'Resend invite';
}): Promise<void> {
return this.telemetry.track(
'Instance sent transactional email to user',
userTransactionalEmailData,
);
}

async onUserPasswordResetRequestClick(userPasswordResetData: { user_id: string }): Promise<void> {
return this.telemetry.track(
'User requested password reset while logged out',
userPasswordResetData,
);
}

async onInstanceOwnerSetup(instanceOwnerSetupData: { user_id: string }): Promise<void> {
return this.telemetry.track('Owner finished instance setup', instanceOwnerSetupData);
}

async onUserSignup(userSignupData: { user_id: string }): Promise<void> {
return this.telemetry.track('User signed up', userSignupData);
}
}
8 changes: 6 additions & 2 deletions packages/cli/src/Server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -767,7 +767,7 @@ class App {
}

await this.externalHooks.run('workflow.afterCreate', [savedWorkflow]);
void InternalHooksManager.getInstance().onWorkflowCreated(newWorkflow);
void InternalHooksManager.getInstance().onWorkflowCreated(req.user.id, newWorkflow);

const { id, ...rest } = savedWorkflow;

Expand Down Expand Up @@ -1101,7 +1101,7 @@ class App {

await Db.collections.Workflow!.delete(workflowId);

void InternalHooksManager.getInstance().onWorkflowDeleted(workflowId);
void InternalHooksManager.getInstance().onWorkflowDeleted(req.user.id, workflowId);
await this.externalHooks.run('workflow.afterDelete', [workflowId]);

return true;
Expand Down Expand Up @@ -2904,6 +2904,10 @@ export async function start(): Promise<void> {
},
deploymentType: config.get('deployment.type'),
binaryDataMode: binarDataConfig.mode,
n8n_multi_user_allowed:
config.get('userManagement.disabled') === false ||
config.get('userManagement.hasOwner') === true,
smtp_set_up: config.get('userManagement.emails.mode') === 'smtp',
krynble marked this conversation as resolved.
Show resolved Hide resolved
};

void Db.collections
Expand Down
8 changes: 5 additions & 3 deletions packages/cli/src/UserManagement/routes/auth.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/naming-convention */
/* eslint-disable import/no-cycle */
/* eslint-disable @typescript-eslint/no-non-null-assertion */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
Expand All @@ -10,6 +11,7 @@ import { issueCookie, resolveJwt } from '../auth/jwt';
import { N8nApp, PublicUser } from '../Interfaces';
import { isInstanceOwnerSetup, sanitizeUser } from '../UserManagementHelper';
import { User } from '../../databases/entities/User';
import type { LoginRequest } from '../../requests';

export function authenticationMethods(this: N8nApp): void {
/**
Expand All @@ -19,7 +21,7 @@ export function authenticationMethods(this: N8nApp): void {
*/
this.app.post(
`/${this.restEndpoint}/login`,
ResponseHelper.send(async (req: Request, res: Response): Promise<PublicUser> => {
ResponseHelper.send(async (req: LoginRequest, res: Response): Promise<PublicUser> => {
if (!req.body.email) {
throw new Error('Email is required to log in');
}
Expand All @@ -32,7 +34,7 @@ export function authenticationMethods(this: N8nApp): void {
try {
user = await Db.collections.User!.findOne(
{
email: req.body.email as string,
email: req.body.email,
},
{
relations: ['globalRole'],
Expand Down Expand Up @@ -107,7 +109,7 @@ export function authenticationMethods(this: N8nApp): void {
*/
this.app.post(
`/${this.restEndpoint}/logout`,
ResponseHelper.send(async (req: Request, res: Response): Promise<IDataObject> => {
ResponseHelper.send(async (_, res: Response): Promise<IDataObject> => {
res.clearCookie(AUTH_COOKIE_NAME);
return {
loggedOut: true,
Expand Down
18 changes: 17 additions & 1 deletion packages/cli/src/UserManagement/routes/me.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import express = require('express');
import validator from 'validator';
import { LoggerProxy as Logger } from 'n8n-workflow';

import { Db, ResponseHelper } from '../..';
import { Db, InternalHooksManager, ResponseHelper } from '../..';
import { issueCookie } from '../auth/jwt';
import { N8nApp, PublicUser } from '../Interfaces';
import { validatePassword, sanitizeUser } from '../UserManagementHelper';
Expand Down Expand Up @@ -60,6 +60,12 @@ export function meNamespace(this: N8nApp): void {

await issueCookie(res, user);

const updatedkeys = Object.keys(req.body);
void InternalHooksManager.getInstance().onUserUpdate({
user_id: req.user.id,
fields_changed: updatedkeys,
});

return sanitizeUser(user);
},
),
Expand All @@ -78,6 +84,11 @@ export function meNamespace(this: N8nApp): void {

await issueCookie(res, user);

void InternalHooksManager.getInstance().onUserUpdate({
user_id: req.user.id,
fields_changed: ['password'],
});

return { success: true };
}),
);
Expand Down Expand Up @@ -111,6 +122,11 @@ export function meNamespace(this: N8nApp): void {

Logger.info('User survey updated successfully', { userId: req.user.id });

void InternalHooksManager.getInstance().onPersonalizationSurveySubmitted(
req.user.id,
personalizationAnswers,
);

return { success: true };
}),
);
Expand Down
6 changes: 5 additions & 1 deletion packages/cli/src/UserManagement/routes/owner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import * as express from 'express';
import validator from 'validator';
import { LoggerProxy as Logger } from 'n8n-workflow';

import { Db, ResponseHelper } from '../..';
import { Db, InternalHooksManager, ResponseHelper } from '../..';
import config = require('../../../config');
import { User } from '../../databases/entities/User';
import { validateEntity } from '../../GenericHelpers';
Expand Down Expand Up @@ -90,6 +90,10 @@ export function ownerNamespace(this: N8nApp): void {

await issueCookie(res, owner);

void InternalHooksManager.getInstance().onInstanceOwnerSetup({
user_id: userId,
});

return sanitizeUser(owner);
}),
);
Expand Down
13 changes: 12 additions & 1 deletion packages/cli/src/UserManagement/routes/passwordReset.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import validator from 'validator';
import { IsNull, MoreThanOrEqual, Not } from 'typeorm';
import { LoggerProxy as Logger } from 'n8n-workflow';

import { Db, ResponseHelper } from '../..';
import { Db, InternalHooksManager, ResponseHelper } from '../..';
import { N8nApp } from '../Interfaces';
import { validatePassword } from '../UserManagementHelper';
import * as UserManagementMailer from '../email';
Expand Down Expand Up @@ -87,6 +87,14 @@ export function passwordResetNamespace(this: N8nApp): void {
});

Logger.info('Sent password reset email successfully', { userId: user.id, email });
void InternalHooksManager.getInstance().onUserTransactionalEmail({
user_id: id,
message_type: 'Reset password',
});

void InternalHooksManager.getInstance().onUserPasswordResetRequestClick({
user_id: id,
});
}),
);

Expand Down Expand Up @@ -131,6 +139,9 @@ export function passwordResetNamespace(this: N8nApp): void {
}

Logger.info('Reset-password token resolved successfully', { userId: id });
void InternalHooksManager.getInstance().onUserPasswordResetEmailClick({
user_id: id,
});
}),
);

Expand Down
Loading