feat: Rewrite Front End cloud and posthog hooks using TypeScript (no-changelog) (#5491)
This commit is contained in:
513
packages/editor-ui/src/hooks/cloud.ts
Normal file
513
packages/editor-ui/src/hooks/cloud.ts
Normal file
@@ -0,0 +1,513 @@
|
||||
import { hooksAddAdminIcon, 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: [
|
||||
() => {
|
||||
hooksAddAdminIcon();
|
||||
},
|
||||
() => {
|
||||
hooksAddFakeDoorFeatures();
|
||||
},
|
||||
],
|
||||
},
|
||||
nodeView: {
|
||||
mount: [
|
||||
() => {
|
||||
const segmentStore = useSegment();
|
||||
segmentStore.identify();
|
||||
},
|
||||
() => {
|
||||
hooksAddAdminIcon();
|
||||
},
|
||||
],
|
||||
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 '';
|
||||
});
|
||||
}
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
4
packages/editor-ui/src/hooks/index.ts
Normal file
4
packages/editor-ui/src/hooks/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export * from './cloud';
|
||||
export * from './segment';
|
||||
export * from './types';
|
||||
export * from './utils';
|
||||
1
packages/editor-ui/src/hooks/init.ts
Normal file
1
packages/editor-ui/src/hooks/init.ts
Normal file
@@ -0,0 +1 @@
|
||||
window.n8nHooksNext = true;
|
||||
351
packages/editor-ui/src/hooks/segment/getters.ts
Normal file
351
packages/editor-ui/src/hooks/segment/getters.ts
Normal file
@@ -0,0 +1,351 @@
|
||||
import { deepCopy } from 'n8n-workflow';
|
||||
import type {
|
||||
ExecutionError,
|
||||
GenericValue,
|
||||
INodeParameters,
|
||||
INodeProperties,
|
||||
ITelemetryTrackProperties,
|
||||
NodeParameterValue,
|
||||
INode,
|
||||
} from 'n8n-workflow';
|
||||
import { useNDVStore } from '@/stores/ndv.store';
|
||||
import type { TelemetryEventData } from '@/hooks/types';
|
||||
import type { INodeUi } from '@/Interface';
|
||||
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||
import { useRootStore } from '@/stores/n8nRoot.store';
|
||||
|
||||
export interface UserSavedCredentialsEventData {
|
||||
credential_type: string;
|
||||
credential_id: string;
|
||||
is_new: boolean;
|
||||
}
|
||||
|
||||
export const getUserSavedCredentialsEventData = (meta: UserSavedCredentialsEventData) => {
|
||||
const rootStore = useRootStore();
|
||||
const workflowsStore = useWorkflowsStore();
|
||||
|
||||
return {
|
||||
eventName: 'User saved credentials',
|
||||
properties: {
|
||||
instance_id: rootStore.instanceId,
|
||||
credential_type: meta.credential_type,
|
||||
credential_id: meta.credential_id,
|
||||
workflow_id: workflowsStore.workflowId,
|
||||
node_type: workflowsStore.activeNode?.name,
|
||||
is_new: meta.is_new,
|
||||
// is_complete: true,
|
||||
// is_valid: true,
|
||||
// error_message: ''
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const getOpenWorkflowSettingsEventData = (): TelemetryEventData => {
|
||||
const workflowsStore = useWorkflowsStore();
|
||||
|
||||
return {
|
||||
eventName: 'User opened workflow settings',
|
||||
properties: {
|
||||
workflow_id: workflowsStore.workflowId,
|
||||
workflow_name: workflowsStore.workflowName,
|
||||
current_settings: deepCopy(workflowsStore.workflowSettings),
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export interface UpdatedWorkflowSettingsEventData {
|
||||
oldSettings: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export const getUpdatedWorkflowSettingsEventData = (
|
||||
meta: UpdatedWorkflowSettingsEventData,
|
||||
): TelemetryEventData => {
|
||||
const workflowsStore = useWorkflowsStore();
|
||||
|
||||
return {
|
||||
eventName: 'User updated workflow settings',
|
||||
properties: {
|
||||
workflow_id: workflowsStore.workflowId,
|
||||
workflow_name: workflowsStore.workflowName,
|
||||
new_settings: deepCopy(workflowsStore.workflowSettings),
|
||||
old_settings: meta.oldSettings,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export interface NodeTypeChangedEventData {
|
||||
nodeSubtitle?: string;
|
||||
}
|
||||
|
||||
export const getNodeTypeChangedEventData = (meta: NodeTypeChangedEventData): TelemetryEventData => {
|
||||
const store = useNDVStore();
|
||||
|
||||
return {
|
||||
eventName: 'User opened node modal',
|
||||
properties: {
|
||||
node_name: store.activeNode?.name,
|
||||
node_subtitle: meta.nodeSubtitle,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export interface InsertedItemFromExpEditorEventData {
|
||||
parameter: {
|
||||
displayName: string;
|
||||
};
|
||||
value: string;
|
||||
selectedItem: {
|
||||
variable: string;
|
||||
};
|
||||
}
|
||||
|
||||
export const getInsertedItemFromExpEditorEventData = (
|
||||
meta: InsertedItemFromExpEditorEventData,
|
||||
): TelemetryEventData => {
|
||||
const store = useNDVStore();
|
||||
|
||||
return {
|
||||
eventName: 'User inserted item from Expression Editor variable selector',
|
||||
properties: {
|
||||
node_name: store.activeNode?.name,
|
||||
node_type: store.activeNode?.type.split('.')[1],
|
||||
parameter_name: meta.parameter.displayName,
|
||||
variable_expression: meta.selectedItem.variable,
|
||||
} as ITelemetryTrackProperties,
|
||||
};
|
||||
};
|
||||
|
||||
export interface ExpressionEditorEventsData {
|
||||
dialogVisible: boolean;
|
||||
value: string;
|
||||
resolvedExpressionValue: string;
|
||||
parameter: INodeParameters;
|
||||
}
|
||||
|
||||
export const getExpressionEditorEventsData = (
|
||||
meta: ExpressionEditorEventsData,
|
||||
isValueDefault: boolean,
|
||||
): TelemetryEventData => {
|
||||
const store = useNDVStore();
|
||||
const eventData: TelemetryEventData = {
|
||||
eventName: '',
|
||||
properties: {},
|
||||
};
|
||||
|
||||
if (!meta.dialogVisible) {
|
||||
eventData.eventName = 'User closed Expression Editor';
|
||||
eventData.properties = {
|
||||
empty_expression: isValueDefault,
|
||||
expression_value: meta.value,
|
||||
expression_result: meta.resolvedExpressionValue.slice(1),
|
||||
};
|
||||
} else {
|
||||
eventData.eventName = 'User opened Expression Editor';
|
||||
eventData.properties = {
|
||||
node_name: store.activeNode?.name,
|
||||
node_type: store.activeNode?.type.split('.')[1],
|
||||
parameter_name: meta.parameter.displayName,
|
||||
parameter_field_type: meta.parameter.type,
|
||||
new_expression: isValueDefault,
|
||||
};
|
||||
}
|
||||
return eventData;
|
||||
};
|
||||
|
||||
export interface AuthenticationModalEventData {
|
||||
parameterPath: string;
|
||||
oldNodeParameters: Record<string, GenericValue>;
|
||||
parameters: INodeProperties[];
|
||||
newValue: NodeParameterValue;
|
||||
}
|
||||
export const getAuthenticationModalEventData = (
|
||||
meta: AuthenticationModalEventData,
|
||||
): TelemetryEventData => {
|
||||
const store = useNDVStore();
|
||||
|
||||
return {
|
||||
eventName: 'User changed Authentication type from node modal',
|
||||
properties: {
|
||||
node_name: store.activeNode?.name,
|
||||
node_type: store.activeNode?.type.split('.')[1],
|
||||
old_mode:
|
||||
meta.oldNodeParameters.authentication ||
|
||||
(
|
||||
meta.parameters.find((param) => param.name === 'authentication') || {
|
||||
default: 'default',
|
||||
}
|
||||
).default,
|
||||
new_mode: meta.newValue,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export interface OutputModeChangedEventData {
|
||||
oldValue: string;
|
||||
newValue: string;
|
||||
}
|
||||
|
||||
export const getOutputModeChangedEventData = (
|
||||
meta: OutputModeChangedEventData,
|
||||
): TelemetryEventData => {
|
||||
const store = useNDVStore();
|
||||
|
||||
return {
|
||||
eventName: 'User changed node output view mode',
|
||||
properties: {
|
||||
old_mode: meta.oldValue,
|
||||
new_mode: meta.newValue,
|
||||
node_name: store.activeNode?.name,
|
||||
node_type: store.activeNode?.type.split('.')[1],
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export interface ExecutionFinishedEventData {
|
||||
runDataExecutedStartData:
|
||||
| { destinationNode?: string | undefined; runNodeFilter?: string[] | undefined }
|
||||
| undefined;
|
||||
nodeName?: string;
|
||||
errorMessage: string;
|
||||
resultDataError: ExecutionError | undefined;
|
||||
itemsCount: number;
|
||||
}
|
||||
|
||||
export const getExecutionFinishedEventData = (
|
||||
meta: ExecutionFinishedEventData,
|
||||
): TelemetryEventData => {
|
||||
const store = useWorkflowsStore();
|
||||
|
||||
const eventData: TelemetryEventData = {
|
||||
eventName: '',
|
||||
properties: {
|
||||
execution_id: store.activeExecutionId,
|
||||
},
|
||||
};
|
||||
|
||||
if (meta.runDataExecutedStartData?.destinationNode) {
|
||||
eventData.eventName = 'Node execution finished';
|
||||
eventData.properties!.node_type = store.getNodeByName(meta.nodeName || '')?.type.split('.')[1];
|
||||
eventData.properties!.node_name = meta.nodeName;
|
||||
} else {
|
||||
eventData.eventName = 'Manual workflow execution finished';
|
||||
eventData.properties!.workflow_id = store.workflowId;
|
||||
eventData.properties!.workflow_name = store.workflowName;
|
||||
}
|
||||
|
||||
if (meta.errorMessage || meta.resultDataError) {
|
||||
eventData.properties!.status = 'failed';
|
||||
eventData.properties!.error_message =
|
||||
(meta.resultDataError && meta.resultDataError.message) || '';
|
||||
eventData.properties!.error_stack = (meta.resultDataError && meta.resultDataError.stack) || '';
|
||||
eventData.properties!.error_ui_message = meta.errorMessage || '';
|
||||
eventData.properties!.error_timestamp = new Date();
|
||||
|
||||
if (meta.resultDataError && (meta.resultDataError as unknown as { node: INodeUi })?.node) {
|
||||
eventData.properties!.error_node =
|
||||
typeof (meta.resultDataError as unknown as { node: string })?.node === 'string'
|
||||
? (meta.resultDataError as unknown as { node: string })?.node
|
||||
: (meta.resultDataError as unknown as { node: INodeUi })?.node?.name;
|
||||
} else {
|
||||
eventData.properties!.error_node = meta.nodeName;
|
||||
}
|
||||
} else {
|
||||
eventData.properties!.status = 'success';
|
||||
if (meta.runDataExecutedStartData?.destinationNode) {
|
||||
// Node execution finished
|
||||
eventData.properties!.items_count = meta.itemsCount || 0;
|
||||
}
|
||||
}
|
||||
return eventData;
|
||||
};
|
||||
|
||||
export interface NodeRemovedEventData {
|
||||
node: INodeUi;
|
||||
}
|
||||
|
||||
export const getNodeRemovedEventData = (meta: NodeRemovedEventData): TelemetryEventData => {
|
||||
const workflowsStore = useWorkflowsStore();
|
||||
|
||||
return {
|
||||
eventName: 'User removed node from workflow canvas',
|
||||
properties: {
|
||||
node_name: meta.node.name,
|
||||
node_type: meta.node.type,
|
||||
node_disabled: meta.node.disabled,
|
||||
workflow_id: workflowsStore.workflowId,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const getNodeEditingFinishedEventData = (
|
||||
activeNode: INode | null,
|
||||
): TelemetryEventData | undefined => {
|
||||
switch (activeNode?.type) {
|
||||
case 'n8n-nodes-base.httpRequest':
|
||||
const domain = (activeNode.parameters.url as string).split('/')[2];
|
||||
return {
|
||||
eventName: 'User finished httpRequest node editing',
|
||||
properties: {
|
||||
method: activeNode.parameters.method,
|
||||
domain,
|
||||
},
|
||||
};
|
||||
case 'n8n-nodes-base.function':
|
||||
return {
|
||||
eventName: 'User finished function node editing',
|
||||
properties: {
|
||||
node_name: activeNode.name,
|
||||
code: activeNode.parameters.functionCode,
|
||||
},
|
||||
};
|
||||
case 'n8n-nodes-base.functionItem':
|
||||
return {
|
||||
eventName: 'User finished functionItem node editing',
|
||||
properties: {
|
||||
node_name: activeNode.name,
|
||||
code: activeNode.parameters.functionCode,
|
||||
},
|
||||
};
|
||||
default:
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
export interface ExecutionStartedEventData {
|
||||
nodeName?: string;
|
||||
source?: string;
|
||||
}
|
||||
|
||||
export const getExecutionStartedEventData = (
|
||||
meta: ExecutionStartedEventData,
|
||||
): TelemetryEventData => {
|
||||
const store = useWorkflowsStore();
|
||||
|
||||
const eventData: TelemetryEventData = {
|
||||
eventName: '',
|
||||
properties: {
|
||||
execution_id: store.activeExecutionId,
|
||||
},
|
||||
};
|
||||
|
||||
// node execution
|
||||
if (meta.nodeName) {
|
||||
eventData.eventName = 'User started node execution';
|
||||
eventData.properties!.source = 'unknown';
|
||||
eventData.properties!.node_type = store.getNodeByName(meta.nodeName)?.type.split('.')[1];
|
||||
eventData.properties!.node_name = meta.nodeName;
|
||||
|
||||
if (meta.source === 'RunData.ExecuteNodeButton') {
|
||||
eventData.properties!.source = 'node_modal';
|
||||
} else if (meta.source === 'Node.executeNode') {
|
||||
eventData.properties!.source = 'workflow_canvas';
|
||||
}
|
||||
} else {
|
||||
// workflow execution
|
||||
eventData.eventName = 'User started manual workflow execution';
|
||||
eventData.properties!.workflow_id = store.workflowId;
|
||||
eventData.properties!.workflow_name = store.workflowName;
|
||||
}
|
||||
|
||||
return eventData;
|
||||
};
|
||||
1
packages/editor-ui/src/hooks/segment/index.ts
Normal file
1
packages/editor-ui/src/hooks/segment/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './getters';
|
||||
6
packages/editor-ui/src/hooks/types.ts
Normal file
6
packages/editor-ui/src/hooks/types.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import type { ITelemetryTrackProperties } from 'n8n-workflow';
|
||||
|
||||
export interface TelemetryEventData {
|
||||
eventName: string;
|
||||
properties?: ITelemetryTrackProperties;
|
||||
}
|
||||
38
packages/editor-ui/src/hooks/utils/hooksAddAdminIcon.ts
Normal file
38
packages/editor-ui/src/hooks/utils/hooksAddAdminIcon.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { useUIStore } from '@/stores/ui.store';
|
||||
import type { IMenuItem } from 'n8n-design-system/types';
|
||||
import { useUsersStore } from '@/stores/users.store';
|
||||
import { addAutoLoginToAdminPanelButton } from '@/hooks/utils/hooksAddAutoLoginToAdminPanelButton';
|
||||
|
||||
let adminIconAdded = false;
|
||||
|
||||
export const hooksAddAdminIcon = () => {
|
||||
if (adminIconAdded) {
|
||||
return;
|
||||
}
|
||||
|
||||
const uiStore = useUIStore();
|
||||
const usersStore = useUsersStore();
|
||||
|
||||
if (usersStore?.globalRoleName !== 'owner') {
|
||||
return;
|
||||
}
|
||||
|
||||
const menuItems: IMenuItem[] = [
|
||||
{
|
||||
id: 'admin',
|
||||
type: 'link',
|
||||
position: 'bottom',
|
||||
label: 'Admin Panel',
|
||||
icon: 'home',
|
||||
properties: {
|
||||
href: 'https://app.n8n.cloud',
|
||||
newWindow: false,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
addAutoLoginToAdminPanelButton();
|
||||
|
||||
uiStore.sidebarMenuItems = [...uiStore.sidebarMenuItems, ...menuItems] as IMenuItem[];
|
||||
adminIconAdded = true;
|
||||
};
|
||||
@@ -0,0 +1,15 @@
|
||||
export function addAutoLoginToAdminPanelButton() {
|
||||
const adminPanelHost = new URL(window.location.href).host.split('.').slice(1).join('.');
|
||||
|
||||
document.body?.addEventListener('click', async (e) => {
|
||||
if (!e.target || !(e.target instanceof Element)) return;
|
||||
if (e.target.getAttribute('id') !== 'admin' && !e.target.closest('#admin')) return;
|
||||
|
||||
e.preventDefault();
|
||||
|
||||
const restPath = window.REST_ENDPOINT ?? 'rest';
|
||||
const response = await fetch(`/${restPath}/cloud/proxy/login/code`);
|
||||
const { code } = await response.json();
|
||||
window.location.href = `https://${adminPanelHost}/login?code=${code}`;
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import { useUIStore } from '@/stores/ui.store';
|
||||
import type { IFakeDoor } from '@/Interface';
|
||||
import { FAKE_DOOR_FEATURES } from '@/constants';
|
||||
|
||||
export function compileFakeDoorFeatures(): IFakeDoor[] {
|
||||
const store = useUIStore();
|
||||
const fakeDoorFeatures: IFakeDoor[] = store.fakeDoorFeatures.map((feature) => ({ ...feature }));
|
||||
|
||||
const environmentsFeature = fakeDoorFeatures.find(
|
||||
(feature) => feature.id === FAKE_DOOR_FEATURES.ENVIRONMENTS,
|
||||
);
|
||||
if (environmentsFeature) {
|
||||
environmentsFeature.actionBoxTitle += '.cloud';
|
||||
environmentsFeature.linkURL += '&edition=cloud';
|
||||
}
|
||||
|
||||
const loggingFeature = fakeDoorFeatures.find(
|
||||
(feature) => feature.id === FAKE_DOOR_FEATURES.LOGGING,
|
||||
);
|
||||
if (loggingFeature) {
|
||||
loggingFeature.actionBoxTitle += '.cloud';
|
||||
loggingFeature.linkURL += '&edition=cloud';
|
||||
loggingFeature.infoText = '';
|
||||
}
|
||||
|
||||
return fakeDoorFeatures;
|
||||
}
|
||||
|
||||
export const hooksAddFakeDoorFeatures = () => {
|
||||
const store = useUIStore();
|
||||
|
||||
store.fakeDoorFeatures = compileFakeDoorFeatures();
|
||||
};
|
||||
30
packages/editor-ui/src/hooks/utils/hooksNodesPanel.ts
Normal file
30
packages/editor-ui/src/hooks/utils/hooksNodesPanel.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
export const nodesPanelSession = {
|
||||
sessionId: '',
|
||||
data: {
|
||||
nodeFilter: '',
|
||||
resultsNodes: [] as string[],
|
||||
filterMode: 'Regular',
|
||||
},
|
||||
};
|
||||
|
||||
export const hooksGenerateNodesPanelEvent = () => {
|
||||
return {
|
||||
eventName: 'User entered nodes panel search term',
|
||||
properties: {
|
||||
search_string: nodesPanelSession.data.nodeFilter,
|
||||
results_count: nodesPanelSession.data.resultsNodes.length,
|
||||
results_nodes: nodesPanelSession.data.resultsNodes,
|
||||
filter_mode: nodesPanelSession.data.filterMode,
|
||||
nodes_panel_session_id: nodesPanelSession.sessionId,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const hooksResetNodesPanelSession = () => {
|
||||
nodesPanelSession.sessionId = `nodes_panel_session_${new Date().valueOf()}`;
|
||||
nodesPanelSession.data = {
|
||||
nodeFilter: '',
|
||||
resultsNodes: [],
|
||||
filterMode: 'Regular',
|
||||
};
|
||||
};
|
||||
4
packages/editor-ui/src/hooks/utils/index.ts
Normal file
4
packages/editor-ui/src/hooks/utils/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export * from './hooksAddAdminIcon';
|
||||
export * from './hooksAddAutoLoginToAdminPanelButton';
|
||||
export * from './hooksAddFakeDoorFeatures';
|
||||
export * from './hooksNodesPanel';
|
||||
Reference in New Issue
Block a user