mirror of
https://github.com/n8n-io/n8n.git
synced 2026-06-02 17:57:06 +02:00
## Summary Provide details about your pull request and what it adds, fixes, or changes. Photos and videos are recommended. In specific scenarios `meta.value` can come as `undefined`, causing the hook to fail. #### How to test the change: 1. Close Expression editor modal dialog with empty value ## Issues fixed Include links to Github issue or Community forum post or **Linear ticket**: > Important in order to close automatically and provide context to reviewers https://linear.app/n8n/issue/N8N-7084/typeerror-evalueslice-is-not-a-function-in-evalueslice1-evalueslice-is https://n8nio.sentry.io/issues/4715787194/ ## Review / Merge checklist - [x] PR title and summary are descriptive. **Remember, the title automatically goes into the changelog. Use `(no-changelog)` otherwise.** ([conventions](https://github.com/n8n-io/n8n/blob/master/.github/pull_request_title_conventions.md)) - [ ] [Docs updated](https://github.com/n8n-io/n8n-docs) or follow-up ticket created. - [ ] Tests included. > A bug is not considered fixed, unless a test is added to prevent it from happening again. A feature is not complete without tests. > > *(internal)* You can use Slack commands to trigger [e2e tests](https://www.notion.so/n8n/How-to-use-Test-Instances-d65f49dfc51f441ea44367fb6f67eb0a?pvs=4#a39f9e5ba64a48b58a71d81c837e8227) or [deploy test instance](https://www.notion.so/n8n/How-to-use-Test-Instances-d65f49dfc51f441ea44367fb6f67eb0a?pvs=4#f6a177d32bde4b57ae2da0b8e454bfce) or [deploy early access version on Cloud](https://www.notion.so/n8n/Cloudbot-3dbe779836004972b7057bc989526998?pvs=4#fef2d36ab02247e1a0f65a74f6fb534e).
508 lines
13 KiB
TypeScript
508 lines
13 KiB
TypeScript
import { hooksAddFakeDoorFeatures } from '@/hooks/utils';
|
|
import {
|
|
getAuthenticationModalEventData,
|
|
getExpressionEditorEventsData,
|
|
getInsertedItemFromExpEditorEventData,
|
|
getNodeTypeChangedEventData,
|
|
getOpenWorkflowSettingsEventData,
|
|
getOutputModeChangedEventData,
|
|
getUpdatedWorkflowSettingsEventData,
|
|
getUserSavedCredentialsEventData,
|
|
getExecutionFinishedEventData,
|
|
getNodeRemovedEventData,
|
|
getNodeEditingFinishedEventData,
|
|
getExecutionStartedEventData,
|
|
} from '@/hooks/segment';
|
|
import { useNDVStore } from '@/stores/ndv.store';
|
|
import { useWorkflowsStore } from '@/stores/workflows.store';
|
|
import {
|
|
hooksGenerateNodesPanelEvent,
|
|
hooksResetNodesPanelSession,
|
|
nodesPanelSession,
|
|
} from '@/hooks/utils/hooksNodesPanel';
|
|
import { useSegment } from '@/stores/segment.store';
|
|
import type { PartialDeep } from 'type-fest';
|
|
import type { IDataObject } from 'n8n-workflow';
|
|
import type { INodeUi } from '@/Interface';
|
|
import type { ExternalHooks } from '@/types';
|
|
|
|
export const n8nCloudHooks: PartialDeep<ExternalHooks> = {
|
|
app: {
|
|
mount: [
|
|
() => {
|
|
hooksAddFakeDoorFeatures();
|
|
},
|
|
],
|
|
},
|
|
nodeView: {
|
|
mount: [
|
|
() => {
|
|
const segmentStore = useSegment();
|
|
segmentStore.identify();
|
|
},
|
|
],
|
|
createNodeActiveChanged: [
|
|
(_, meta) => {
|
|
const segmentStore = useSegment();
|
|
const eventData = {
|
|
source: meta.source,
|
|
nodes_panel_session_id: nodesPanelSession.sessionId,
|
|
};
|
|
|
|
hooksResetNodesPanelSession();
|
|
segmentStore.track('User opened nodes panel', eventData);
|
|
segmentStore.page('Cloud instance', 'Nodes panel', eventData);
|
|
},
|
|
],
|
|
addNodeButton: [
|
|
(_, meta) => {
|
|
const segmentStore = useSegment();
|
|
const eventData = {
|
|
eventName: 'User added node to workflow canvas',
|
|
properties: {
|
|
node_type: meta.nodeTypeName.split('.')[1],
|
|
nodes_panel_session_id: nodesPanelSession.sessionId,
|
|
},
|
|
};
|
|
|
|
segmentStore.track(eventData.eventName, eventData.properties);
|
|
},
|
|
],
|
|
},
|
|
main: {
|
|
routeChange: [
|
|
(_, meta) => {
|
|
const segmentStore = useSegment();
|
|
const splitPath = meta.to.path.split('/');
|
|
if (meta.from.path !== '/' && splitPath[1] === 'workflow') {
|
|
const eventData = {
|
|
workflow_id: splitPath[2],
|
|
};
|
|
|
|
segmentStore.page('Cloud instance', 'Workflow editor', eventData);
|
|
}
|
|
},
|
|
],
|
|
},
|
|
credential: {
|
|
saved: [
|
|
(_, meta) => {
|
|
const segmentStore = useSegment();
|
|
const eventData = getUserSavedCredentialsEventData(meta);
|
|
|
|
segmentStore.track(eventData.eventName, eventData.properties);
|
|
},
|
|
],
|
|
},
|
|
credentialsEdit: {
|
|
credentialTypeChanged: [
|
|
(_, meta) => {
|
|
const segmentStore = useSegment();
|
|
if (meta.newValue) {
|
|
const eventData = {
|
|
eventName: 'User opened Credentials modal',
|
|
properties: {
|
|
source: meta.setCredentialType === meta.credentialType ? 'node' : 'primary_menu',
|
|
new_credential: !meta.editCredentials,
|
|
credential_type: meta.credentialType,
|
|
},
|
|
};
|
|
|
|
segmentStore.track(eventData.eventName, eventData.properties);
|
|
segmentStore.page('Cloud instance', 'Credentials modal', eventData.properties);
|
|
}
|
|
},
|
|
],
|
|
credentialModalOpened: [
|
|
(_, meta) => {
|
|
const segmentStore = useSegment();
|
|
const eventData = {
|
|
eventName: 'User opened Credentials modal',
|
|
properties: {
|
|
source: meta.activeNode ? 'node' : 'primary_menu',
|
|
new_credential: !meta.isEditingCredential,
|
|
credential_type: meta.credentialType,
|
|
},
|
|
};
|
|
|
|
segmentStore.track(eventData.eventName, eventData.properties);
|
|
segmentStore.page('Cloud instance', 'Credentials modal', eventData.properties);
|
|
},
|
|
],
|
|
},
|
|
credentialsList: {
|
|
mounted: [
|
|
() => {
|
|
const segmentStore = useSegment();
|
|
const eventData = {
|
|
eventName: 'User opened global Credentials panel',
|
|
};
|
|
|
|
segmentStore.track(eventData.eventName);
|
|
segmentStore.page('Cloud instance', 'Credentials panel');
|
|
},
|
|
],
|
|
dialogVisibleChanged: [
|
|
(_, meta) => {
|
|
const segmentStore = useSegment();
|
|
if (meta.dialogVisible) {
|
|
const eventData = {
|
|
eventName: 'User opened global Credentials panel',
|
|
};
|
|
|
|
segmentStore.track(eventData.eventName);
|
|
segmentStore.page('Cloud instance', 'Credentials panel');
|
|
}
|
|
},
|
|
],
|
|
},
|
|
workflowSettings: {
|
|
dialogVisibleChanged: [
|
|
(_, meta) => {
|
|
const segmentStore = useSegment();
|
|
if (meta.dialogVisible) {
|
|
const eventData = getOpenWorkflowSettingsEventData();
|
|
segmentStore.track(eventData.eventName, eventData.properties);
|
|
}
|
|
},
|
|
],
|
|
saveSettings: [
|
|
(_, meta) => {
|
|
const segmentStore = useSegment();
|
|
const eventData = getUpdatedWorkflowSettingsEventData(meta);
|
|
segmentStore.track(eventData.eventName, eventData.properties);
|
|
},
|
|
],
|
|
},
|
|
dataDisplay: {
|
|
onDocumentationUrlClick: [
|
|
(_, meta) => {
|
|
const segmentStore = useSegment();
|
|
const eventData = {
|
|
eventName: 'User clicked node modal docs link',
|
|
properties: {
|
|
node_type: meta.nodeType.name.split('.')[1],
|
|
docs_link: meta.documentationUrl,
|
|
},
|
|
};
|
|
|
|
segmentStore.track(eventData.eventName, eventData.properties);
|
|
},
|
|
],
|
|
nodeTypeChanged: [
|
|
(_, meta) => {
|
|
const segmentStore = useSegment();
|
|
const ndvStore = useNDVStore();
|
|
const eventData = getNodeTypeChangedEventData(meta);
|
|
|
|
segmentStore.track(eventData.eventName, eventData.properties);
|
|
segmentStore.page('Cloud instance', 'Node modal', {
|
|
node: ndvStore.activeNode?.name,
|
|
});
|
|
},
|
|
],
|
|
nodeEditingFinished: [
|
|
() => {
|
|
const segmentStore = useSegment();
|
|
const ndvStore = useNDVStore();
|
|
const workflowsStore = useWorkflowsStore();
|
|
|
|
const eventData = getNodeEditingFinishedEventData(ndvStore.activeNode);
|
|
if (eventData) {
|
|
eventData.properties!.workflow_id = workflowsStore.workflowId;
|
|
}
|
|
|
|
if (eventData) {
|
|
segmentStore.track(eventData.eventName, eventData.properties);
|
|
}
|
|
},
|
|
],
|
|
},
|
|
executionsList: {
|
|
openDialog: [
|
|
() => {
|
|
const segmentStore = useSegment();
|
|
const eventData = {
|
|
eventName: 'User opened Executions log',
|
|
};
|
|
|
|
segmentStore.track(eventData.eventName);
|
|
segmentStore.page('Cloud instance', 'Executions log');
|
|
},
|
|
],
|
|
},
|
|
showMessage: {
|
|
showError: [
|
|
(_, meta) => {
|
|
const segmentStore = useSegment();
|
|
const eventData = {
|
|
eventName: 'Instance FE emitted error',
|
|
properties: {
|
|
error_title: meta.title,
|
|
error_description: meta.message,
|
|
error_message: meta.errorMessage,
|
|
},
|
|
};
|
|
|
|
segmentStore.track(eventData.eventName, eventData.properties);
|
|
},
|
|
],
|
|
},
|
|
expressionEdit: {
|
|
itemSelected: [
|
|
(_, meta) => {
|
|
const segmentStore = useSegment();
|
|
const eventData = getInsertedItemFromExpEditorEventData(meta);
|
|
|
|
if (meta.selectedItem.variable.startsWith('Object.keys')) {
|
|
eventData.properties!.variable_type = 'Keys';
|
|
} else if (meta.selectedItem.variable.startsWith('Object.values')) {
|
|
eventData.properties!.variable_type = 'Values';
|
|
} else {
|
|
eventData.properties!.variable_type = 'Raw value';
|
|
}
|
|
|
|
segmentStore.track(eventData.eventName, eventData.properties);
|
|
},
|
|
],
|
|
dialogVisibleChanged: [
|
|
(_, meta) => {
|
|
const segmentStore = useSegment();
|
|
const currentValue = meta.value?.slice(1) ?? '';
|
|
let isValueDefault = false;
|
|
|
|
switch (typeof meta.parameter.default) {
|
|
case 'boolean':
|
|
isValueDefault =
|
|
(currentValue === 'true' && meta.parameter.default) ||
|
|
(currentValue === 'false' && !meta.parameter.default);
|
|
break;
|
|
case 'string':
|
|
isValueDefault = currentValue === meta.parameter.default;
|
|
break;
|
|
case 'number':
|
|
isValueDefault = currentValue === meta.parameter.default.toString();
|
|
break;
|
|
}
|
|
|
|
const eventData = getExpressionEditorEventsData(meta, isValueDefault);
|
|
|
|
segmentStore.track(eventData.eventName, eventData.properties);
|
|
},
|
|
],
|
|
},
|
|
nodeSettings: {
|
|
valueChanged: [
|
|
(_, meta) => {
|
|
const segmentStore = useSegment();
|
|
if (meta.parameterPath !== 'authentication') {
|
|
return;
|
|
}
|
|
|
|
const eventData = getAuthenticationModalEventData(meta);
|
|
|
|
segmentStore.track(eventData.eventName, eventData.properties);
|
|
},
|
|
],
|
|
credentialSelected: [
|
|
(_, meta) => {
|
|
const segmentStore = useSegment();
|
|
const creds = Object.keys(meta.updateInformation.properties.credentials || {});
|
|
if (creds.length < 1) {
|
|
return;
|
|
}
|
|
|
|
const eventData = {
|
|
eventName: 'User selected credential from node modal',
|
|
properties: {
|
|
credential_name: (meta.updateInformation.properties.credentials as IDataObject)[
|
|
creds[0]
|
|
],
|
|
credential_type: creds[0],
|
|
},
|
|
};
|
|
|
|
segmentStore.track(eventData.eventName, eventData.properties);
|
|
},
|
|
],
|
|
},
|
|
workflowRun: {
|
|
runWorkflow: [
|
|
(_, meta) => {
|
|
const segmentStore = useSegment();
|
|
const eventData = getExecutionStartedEventData(meta);
|
|
|
|
segmentStore.track(eventData.eventName, eventData.properties);
|
|
},
|
|
],
|
|
runError: [
|
|
(_, meta) => {
|
|
const segmentStore = useSegment();
|
|
const eventData = {
|
|
eventName: meta.nodeName
|
|
? 'Node execution finished'
|
|
: 'Manual workflow execution finished',
|
|
properties: {
|
|
preflight: 'true',
|
|
status: 'failed',
|
|
error_message: meta.errorMessages.join('<br /> - '),
|
|
error_timestamp: new Date(),
|
|
node_name: meta.nodeName,
|
|
},
|
|
};
|
|
|
|
segmentStore.track(eventData.eventName, eventData.properties);
|
|
},
|
|
],
|
|
},
|
|
runData: {
|
|
displayModeChanged: [
|
|
(_, meta) => {
|
|
const segmentStore = useSegment();
|
|
const eventData = getOutputModeChangedEventData(meta);
|
|
|
|
segmentStore.track(eventData.eventName, eventData.properties);
|
|
},
|
|
],
|
|
},
|
|
pushConnection: {
|
|
executionFinished: [
|
|
(_, meta) => {
|
|
const segmentStore = useSegment();
|
|
const eventData = getExecutionFinishedEventData(meta);
|
|
|
|
segmentStore.track(eventData.eventName, eventData.properties);
|
|
},
|
|
],
|
|
},
|
|
node: {
|
|
deleteNode: [
|
|
(_, meta) => {
|
|
const segmentStore = useSegment();
|
|
const eventData = getNodeRemovedEventData(meta);
|
|
|
|
segmentStore.track(eventData.eventName, eventData.properties);
|
|
},
|
|
],
|
|
},
|
|
workflow: {
|
|
activeChange: [
|
|
(_, meta) => {
|
|
const segmentStore = useSegment();
|
|
const eventData = {
|
|
eventName: (meta.active && 'User activated workflow') || 'User deactivated workflow',
|
|
properties: {
|
|
workflow_id: meta.workflowId,
|
|
source: 'workflow_modal',
|
|
},
|
|
};
|
|
|
|
segmentStore.track(eventData.eventName, eventData.properties);
|
|
},
|
|
],
|
|
activeChangeCurrent: [
|
|
(_, meta) => {
|
|
const segmentStore = useSegment();
|
|
const workflowsStore = useWorkflowsStore();
|
|
|
|
const eventData = {
|
|
eventName: (meta.active && 'User activated workflow') || 'User deactivated workflow',
|
|
properties: {
|
|
source: 'main nav',
|
|
workflow_id: meta.workflowId,
|
|
workflow_name: workflowsStore.workflowName,
|
|
workflow_nodes: workflowsStore.allNodes.map((n) => n.type.split('.')[1]),
|
|
},
|
|
};
|
|
|
|
segmentStore.track(eventData.eventName, eventData.properties);
|
|
},
|
|
],
|
|
afterUpdate: [
|
|
(_, meta) => {
|
|
const segmentStore = useSegment();
|
|
const eventData = {
|
|
eventName: 'User saved workflow',
|
|
properties: {
|
|
workflow_id: meta.workflowData.id,
|
|
workflow_name: meta.workflowData.name,
|
|
workflow_nodes: meta.workflowData.nodes.map((n) => n.type.split('.')[1]),
|
|
},
|
|
};
|
|
|
|
segmentStore.track(eventData.eventName, eventData.properties);
|
|
},
|
|
],
|
|
},
|
|
execution: {
|
|
open: [
|
|
(_, meta) => {
|
|
const segmentStore = useSegment();
|
|
const eventData = {
|
|
eventName: 'User opened read-only execution',
|
|
properties: {
|
|
workflow_id: meta.workflowId,
|
|
workflow_name: meta.workflowName,
|
|
execution_id: meta.executionId,
|
|
},
|
|
};
|
|
|
|
segmentStore.track(eventData.eventName, eventData.properties);
|
|
},
|
|
],
|
|
},
|
|
nodeCreateList: {
|
|
destroyed: [
|
|
() => {
|
|
const segmentStore = useSegment();
|
|
if (
|
|
nodesPanelSession.data.nodeFilter.length > 0 &&
|
|
nodesPanelSession.data.nodeFilter !== ''
|
|
) {
|
|
const eventData = hooksGenerateNodesPanelEvent();
|
|
|
|
segmentStore.track(eventData.eventName, eventData.properties);
|
|
}
|
|
},
|
|
],
|
|
selectedTypeChanged: [
|
|
(_, meta) => {
|
|
const segmentStore = useSegment();
|
|
const eventData = {
|
|
eventName: 'User changed nodes panel filter',
|
|
properties: {
|
|
old_filter: meta.oldValue,
|
|
new_filter: meta.newValue,
|
|
nodes_panel_session_id: nodesPanelSession.sessionId,
|
|
},
|
|
};
|
|
nodesPanelSession.data.filterMode = meta.newValue;
|
|
|
|
segmentStore.track(eventData.eventName, eventData.properties);
|
|
},
|
|
],
|
|
nodeFilterChanged: [
|
|
(_, meta) => {
|
|
const segmentStore = useSegment();
|
|
if (meta.newValue.length === 0 && nodesPanelSession.data.nodeFilter.length > 0) {
|
|
const eventData = hooksGenerateNodesPanelEvent();
|
|
|
|
segmentStore.track(eventData.eventName, eventData.properties);
|
|
}
|
|
|
|
if (meta.newValue.length > meta.oldValue.length) {
|
|
nodesPanelSession.data.nodeFilter = meta.newValue;
|
|
nodesPanelSession.data.resultsNodes = meta.filteredNodes.map((node) => {
|
|
if ((node as unknown as INodeUi).name) {
|
|
return (node as unknown as INodeUi).name.split('.')[1];
|
|
} else if (node.key) {
|
|
return node.key.split('.')[1];
|
|
}
|
|
return '';
|
|
});
|
|
}
|
|
},
|
|
],
|
|
},
|
|
};
|