* ✨ Inicial setup * ⚡ Add authentication handler * ⚡ Add GET /users route * ⚡ Improvements * 👕 Fix linting issues * ⚡ Add GET /users/:identifier endpoint * ⚡ Add POST /users endpoint * ⚡ Add DELETE /users/:identifier endpoint * ⚡ Return error using express native functions * 👕 Fix linting issue * ⚡ Possibility to add custom middleware * ⚡ Refactor POST /users * ⚡ Refactor DELETE /users * ⚡ Improve cleaning function * ⚡ Refactor GET /users and /users/:identifier * ⚡ Add API spec to route * ⚡ Add raw option to response helper * 🐛 Fix issue adding custom middleware * ⚡ Enable includeRole parameter in GET /users/:identifier * ⚡ Fix linting issues after merge * ⚡ Add missing config variable * ⚡ General improvements ⚡ asasas * ⚡ Add POST /users tests * Debug public API tests * Fix both sets of tests * ⚡ Improvements * ⚡ Load api versions dynamically * ⚡ Add endpoints to UM to create/delete an API Key * ⚡ Add index to apiKey column * 👕 Fix linting issue * ⚡ Clean open api spec * ⚡ Improvements * ⚡ Skip tests * 🐛 Fix bug with test * ⚡ Fix issue with the open api spec * ⚡ Fix merge issue * ⚡ Move token enpoints from /users to /me * ⚡ Apply feedback to openapi.yml * ⚡ Improvements to api-key endpoints * 🐛 Fix test to suport API dynamic loading * ⚡ Expose swagger ui in GET /{version}/docs * ⚡ Allow to disable public api via env variable * ⚡ Change handlers structure * 🚧 WIP create credential, delete credential complete * 🐛 fix route for creating api key * ⚡ return api key of authenticated user * ⚡ Expose public api activation to the settings * ⬆️ Update package-lock.json file * ⚡ Add execution resource * ⚡ Fix linting issues * 🛠 conditional public api endpoints excluding * ⚡️ create credential complete * ✨ Added n8n-card component. Added spacing utility classes. * ♻️ Made use of n8n-card in existing components. * ✨ Added api key setup view. * ✨ Added api keys get/create/delete actions. * ✨ Added public api permissions handling. * ♻️ Temporarily disabling card tests. * ♻️ Changed translations. Storing api key only in component. * ✨ Added utilities storybook entry * ♻️ Changed default value for generic copy input. * 🧹 clean up createCredential * ⚡ Add workflow resource to openapi spec * 🐛 Fix naming with env variable * ⚡ Allow multifile openapi spec * ⚡ Add POST /workflows/:workflowId/activate * fix up view, fix issues * remove delete api key modal * remove unused prop * clean up store api * remove getter * remove unused dispatch * fix component size to match * use existing components * match figma closely * fix bug when um is disabled in sidebar * set copy input color * remove unused import * ⚡ Remove css path * ⚡ Add POST /workflows/:workflowId/desactivate * ⚡ Add POST /workflows * Revert "⚡ Remove css path" a3d0a71719834ef37c88c23c83dfe662e96185aa * attempt to fix docker image issue * revert dockerfile test * disable public api * disable api differently * Revert "disable api differently" b70e29433e45934975e41ffdc32e288988aba9b0 * Revert "disable public api" 886e5164fb4135c164f77561bdb4427e5cd44ac1 * remove unused box * ⚡ PUT /workflows/:workflowId * ⚡ Refactor workflow endpoints * ⚡ Refactor executions endpoints * ⚡ Fix typo * ✅ add credentials tests * ✅ adjust users tests * update text * add try it out link * ⚡ Add delete, getAll and get to the workflow resource * address spacing comments * ⚡️ apply correct structure * ⚡ Add missing test to user resource and fix some issues * ⚡ Add workflow tests * ⚡ Add missing workflow tests and fix some issues * ⚡ Executions tests * ⚡ finish execution tests * ⚡ Validate credentials data depending on type * ⚡️ implement review comments * 👕 fix lint issues * ⚡ Add apiKey to sanatizeUser * ⚡ Fix issues with spec and tests * ⚡ Add new structure * ⚡ Validate credentials type and properties * ⚡ Make all endpoints except /users independent on UM * ⚡ Add instance base path to swagger UI * ⚡ Remove testing endpoints * ⚡ Fix issue with openapi tags * ⚡ Add endpoint GET /credentialTypes/:id/schema * 🐛 Fix issue adding json middleware to public api * ⚡ Add API playground path to FE * ⚡ Add telemetry and external hooks * 🐛 Fix issue with user tests * ⚡ Move /credentialTypes under /credentials * ⚡ Add test to GET /credentials/schema/:id * 🛠 refactor schema naming * ⚡ Add DB migrations asas * ✅ add tests for crd apiKey * ✨ Added API View telemetry events. * ⚡ Remove rsync from the building process as it is missing on alpine base image * ⚡ add missing BE telemetry events * 🐛 Fix credential tests * ⚡ address outstanding feedback * 🔨 Remove move:openapi script * ⬆️ update dependency * ⬆️ update package-lock.json * 👕 Fix linting issue * 🐛 Fix package.json issue * 🐛 fix migrations and tests * 🐛 fix typos + naming * 🚧 WIP fixing tests * ⚡ Add json schema validation * ⚡ Add missing fields to node schema * ⚡ Add limit max upper limit * ⚡ Rename id paths * 🐛 Fix tests * Add package-lock.jsonto custom dockerfile * ⬆️ Update package-lock.json * 🐛 Fix issue with build * ✏️ add beta label to api view * 🔥 Remove user endpoints * ⚡ Add schema examples to GET /credentials/schema/:id * 🔥 Remove user endpoints tests * 🐛 Fix tests * 🎨 adapt points from design review * 🔥 remove unnecessary text-align * ⚡️ update UI * 🐛 Fix issue with executions filter * ⚡ Add tags filter to GET /workflows * ⚡ Add missing error messages * ✅ add and update public api tests * ✅ add tests for owner activiating/deactivating non-owned wfs * 🧪 add tests for filter for tags * 🧪 add tests for more filter params * 🐛 fix inclusion of tags * 🛠 enhance readability * ⚡️ small refactorings * 💄 improving readability/naming * ⚡ Set API latest version dinamically * Add comments to toJsonSchema function * ⚡ Fix issue * ⚡ Make execution data usable * ⚡ Fix validation issue * ⚡ Rename data field and change parameter and options * 🐛 Fix issue parameter "detailsFieldFormat" not resolving correctly * Skip executions tests * skip workflow failing test * Rename details property to data * ⚡ Add includeData parameter * 🐛 Fix issue with openapi spec * 🐛 Fix linting issue * ⚡ Fix execution schema Co-authored-by: Iván Ovejero <ivov.src@gmail.com> Co-authored-by: Ben Hesseldieck <b.hesseldieck@gmail.com> Co-authored-by: Alex Grozav <alex@grozav.com> Co-authored-by: Mutasem <mutdmour@gmail.com> Co-authored-by: Jan Oberhauser <jan.oberhauser@gmail.com>
367 lines
11 KiB
TypeScript
367 lines
11 KiB
TypeScript
/* eslint-disable import/no-cycle */
|
|
import { BinaryDataManager } from 'n8n-core';
|
|
import { IDataObject, INodeTypes, IRun, TelemetryHelpers } from 'n8n-workflow';
|
|
import { snakeCase } from 'change-case';
|
|
import {
|
|
IDiagnosticInfo,
|
|
IInternalHooksClass,
|
|
ITelemetryUserDeletionData,
|
|
IWorkflowBase,
|
|
IWorkflowDb,
|
|
} from '.';
|
|
import { Telemetry } from './telemetry';
|
|
|
|
export class InternalHooksClass implements IInternalHooksClass {
|
|
private versionCli: string;
|
|
|
|
private nodeTypes: INodeTypes;
|
|
|
|
constructor(private telemetry: Telemetry, versionCli: string, nodeTypes: INodeTypes) {
|
|
this.versionCli = versionCli;
|
|
this.nodeTypes = nodeTypes;
|
|
}
|
|
|
|
async onServerStarted(
|
|
diagnosticInfo: IDiagnosticInfo,
|
|
earliestWorkflowCreatedAt?: Date,
|
|
): Promise<unknown[]> {
|
|
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,
|
|
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([
|
|
this.telemetry.identify(info),
|
|
this.telemetry.track('Instance started', {
|
|
...info,
|
|
earliest_workflow_created: earliestWorkflowCreatedAt,
|
|
}),
|
|
]);
|
|
}
|
|
|
|
async onPersonalizationSurveySubmitted(
|
|
userId: string,
|
|
answers: Record<string, string>,
|
|
): Promise<void> {
|
|
const camelCaseKeys = Object.keys(answers);
|
|
const personalizationSurveyData = { user_id: userId } as Record<string, string | string[]>;
|
|
camelCaseKeys.forEach((camelCaseKey) => {
|
|
personalizationSurveyData[snakeCase(camelCaseKey)] = answers[camelCaseKey];
|
|
});
|
|
|
|
return this.telemetry.track(
|
|
'User responded to personalization questions',
|
|
personalizationSurveyData,
|
|
);
|
|
}
|
|
|
|
async onWorkflowCreated(
|
|
userId: string,
|
|
workflow: IWorkflowBase,
|
|
publicApi: boolean,
|
|
): 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),
|
|
public_api: publicApi,
|
|
});
|
|
}
|
|
|
|
async onWorkflowDeleted(userId: string, workflowId: string, publicApi: boolean): Promise<void> {
|
|
return this.telemetry.track('User deleted workflow', {
|
|
user_id: userId,
|
|
workflow_id: workflowId,
|
|
public_api: publicApi,
|
|
});
|
|
}
|
|
|
|
async onWorkflowSaved(userId: string, workflow: IWorkflowDb, publicApi: boolean): Promise<void> {
|
|
const { nodeGraph } = TelemetryHelpers.generateNodesGraph(workflow, this.nodeTypes);
|
|
|
|
const notesCount = Object.keys(nodeGraph.notes).length;
|
|
const overlappingCount = Object.values(nodeGraph.notes).filter(
|
|
(note) => note.overlapping,
|
|
).length;
|
|
|
|
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,
|
|
version_cli: this.versionCli,
|
|
num_tags: workflow.tags?.length ?? 0,
|
|
public_api: publicApi,
|
|
});
|
|
}
|
|
|
|
async onWorkflowPostExecute(
|
|
executionId: string,
|
|
workflow: IWorkflowBase,
|
|
runData?: IRun,
|
|
userId?: string,
|
|
): Promise<void> {
|
|
const promises = [Promise.resolve()];
|
|
const properties: IDataObject = {
|
|
workflow_id: workflow.id,
|
|
is_manual: false,
|
|
version_cli: this.versionCli,
|
|
};
|
|
|
|
if (userId) {
|
|
properties.user_id = userId;
|
|
}
|
|
|
|
if (runData !== undefined) {
|
|
properties.execution_mode = runData.mode;
|
|
properties.success = !!runData.finished;
|
|
properties.is_manual = runData.mode === 'manual';
|
|
|
|
let nodeGraphResult;
|
|
|
|
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) {
|
|
nodeGraphResult = TelemetryHelpers.generateNodesGraph(workflow, this.nodeTypes);
|
|
properties.node_graph = nodeGraphResult.nodeGraph;
|
|
properties.node_graph_string = JSON.stringify(nodeGraphResult.nodeGraph);
|
|
|
|
if (errorNodeName) {
|
|
properties.error_node_id = nodeGraphResult.nameIndices[errorNodeName];
|
|
}
|
|
}
|
|
}
|
|
|
|
if (properties.is_manual) {
|
|
if (!nodeGraphResult) {
|
|
nodeGraphResult = TelemetryHelpers.generateNodesGraph(workflow, this.nodeTypes);
|
|
}
|
|
|
|
const manualExecEventProperties = {
|
|
workflow_id: workflow.id,
|
|
status: properties.success ? 'success' : 'failed',
|
|
error_message: properties.error_message,
|
|
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,
|
|
};
|
|
|
|
if (!manualExecEventProperties.node_graph) {
|
|
nodeGraphResult = TelemetryHelpers.generateNodesGraph(workflow, this.nodeTypes);
|
|
manualExecEventProperties.node_graph = nodeGraphResult.nodeGraph;
|
|
manualExecEventProperties.node_graph_string = JSON.stringify(
|
|
manualExecEventProperties.node_graph,
|
|
);
|
|
}
|
|
|
|
if (runData.data.startData?.destinationNode) {
|
|
promises.push(
|
|
this.telemetry.track('Manual node exec finished', {
|
|
...manualExecEventProperties,
|
|
node_type: TelemetryHelpers.getNodeTypeForName(
|
|
workflow,
|
|
runData.data.startData?.destinationNode,
|
|
)?.type,
|
|
node_id: nodeGraphResult.nameIndices[runData.data.startData?.destinationNode],
|
|
}),
|
|
);
|
|
} else {
|
|
promises.push(
|
|
this.telemetry.track('Manual workflow exec finished', manualExecEventProperties),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
return Promise.all([
|
|
...promises,
|
|
BinaryDataManager.getInstance().persistBinaryDataForExecutionId(executionId),
|
|
this.telemetry.trackWorkflowExecution(properties),
|
|
]).then(() => {});
|
|
}
|
|
|
|
async onN8nStop(): Promise<void> {
|
|
const timeoutPromise = new Promise<void>((resolve) => {
|
|
setTimeout(() => {
|
|
resolve();
|
|
}, 3000);
|
|
});
|
|
|
|
return Promise.race([timeoutPromise, this.telemetry.trackN8nStop()]);
|
|
}
|
|
|
|
async onUserDeletion(
|
|
userId: string,
|
|
userDeletionData: ITelemetryUserDeletionData,
|
|
publicApi: boolean,
|
|
): Promise<void> {
|
|
return this.telemetry.track('User deleted user', {
|
|
...userDeletionData,
|
|
user_id: userId,
|
|
public_api: publicApi,
|
|
});
|
|
}
|
|
|
|
async onUserInvite(userInviteData: {
|
|
user_id: string;
|
|
target_user_id: string[];
|
|
public_api: boolean;
|
|
}): Promise<void> {
|
|
return this.telemetry.track('User invited new user', userInviteData);
|
|
}
|
|
|
|
async onUserReinvite(userReinviteData: {
|
|
user_id: string;
|
|
target_user_id: string;
|
|
public_api: boolean;
|
|
}): Promise<void> {
|
|
return this.telemetry.track('User resent new user invite email', userReinviteData);
|
|
}
|
|
|
|
async onUserRetrievedUser(userRetrievedData: {
|
|
user_id: string;
|
|
public_api: boolean;
|
|
}): Promise<void> {
|
|
return this.telemetry.track('User retrieved user', userRetrievedData);
|
|
}
|
|
|
|
async onUserRetrievedAllUsers(userRetrievedData: {
|
|
user_id: string;
|
|
public_api: boolean;
|
|
}): Promise<void> {
|
|
return this.telemetry.track('User retrieved all users', userRetrievedData);
|
|
}
|
|
|
|
async onUserRetrievedExecution(userRetrievedData: {
|
|
user_id: string;
|
|
public_api: boolean;
|
|
}): Promise<void> {
|
|
return this.telemetry.track('User retrieved execution', userRetrievedData);
|
|
}
|
|
|
|
async onUserRetrievedAllExecutions(userRetrievedData: {
|
|
user_id: string;
|
|
public_api: boolean;
|
|
}): Promise<void> {
|
|
return this.telemetry.track('User retrieved all executions', userRetrievedData);
|
|
}
|
|
|
|
async onUserRetrievedWorkflow(userRetrievedData: {
|
|
user_id: string;
|
|
public_api: boolean;
|
|
}): Promise<void> {
|
|
return this.telemetry.track('User retrieved workflow', userRetrievedData);
|
|
}
|
|
|
|
async onUserRetrievedAllWorkflows(userRetrievedData: {
|
|
user_id: string;
|
|
public_api: boolean;
|
|
}): Promise<void> {
|
|
return this.telemetry.track('User retrieved all workflows', userRetrievedData);
|
|
}
|
|
|
|
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';
|
|
public_api: boolean;
|
|
}): Promise<void> {
|
|
return this.telemetry.track(
|
|
'Instance sent transacptional email to user',
|
|
userTransactionalEmailData,
|
|
);
|
|
}
|
|
|
|
async onUserInvokedApi(userInvokedApiData: {
|
|
user_id: string;
|
|
path: string;
|
|
method: string;
|
|
api_version: string;
|
|
}): Promise<void> {
|
|
return this.telemetry.track('User invoked API', userInvokedApiData);
|
|
}
|
|
|
|
async onApiKeyDeleted(apiKeyDeletedData: {
|
|
user_id: string;
|
|
public_api: boolean;
|
|
}): Promise<void> {
|
|
return this.telemetry.track('API key deleted', apiKeyDeletedData);
|
|
}
|
|
|
|
async onApiKeyCreated(apiKeyCreatedData: {
|
|
user_id: string;
|
|
public_api: boolean;
|
|
}): Promise<void> {
|
|
return this.telemetry.track('API key created', apiKeyCreatedData);
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
async onEmailFailed(failedEmailData: {
|
|
user_id: string;
|
|
message_type: 'Reset password' | 'New user invite' | 'Resend invite';
|
|
public_api: boolean;
|
|
}): Promise<void> {
|
|
return this.telemetry.track(
|
|
'Instance failed to send transactional email to user',
|
|
failedEmailData,
|
|
);
|
|
}
|
|
}
|