fix: Bring back nodes panel telemetry events (#11456)

This commit is contained in:
Mutasem Aldmour 2024-10-31 16:29:51 +01:00 committed by GitHub
parent 529d4fc3ef
commit 130c942f63
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 524 additions and 221 deletions

View file

@ -29,6 +29,7 @@ import CategorizedItemsRenderer from '../Renderers/CategorizedItemsRenderer.vue'
import type { IDataObject } from 'n8n-workflow';
import { useTelemetry } from '@/composables/useTelemetry';
import { useI18n } from '@/composables/useI18n';
import { useNodeCreatorStore } from '@/stores/nodeCreator.store';
const emit = defineEmits<{
nodeTypeSelected: [value: [actionKey: string, nodeName: string] | [nodeName: string]];
@ -47,6 +48,8 @@ const {
actionsCategoryLocales,
} = useActions();
const nodeCreatorStore = useNodeCreatorStore();
// We only inject labels if search is empty
const parsedTriggerActions = computed(() =>
parseActions(actions.value, actionsCategoryLocales.value.triggers, false),
@ -182,7 +185,7 @@ function trackActionsView() {
};
void useExternalHooks().run('nodeCreateList.onViewActions', trackingPayload);
telemetry?.trackNodesPanel('nodeCreateList.onViewActions', trackingPayload);
nodeCreatorStore.onViewActions(trackingPayload);
}
function resetSearch() {
@ -206,7 +209,7 @@ function addHttpNode() {
void useExternalHooks().run('nodeCreateList.onActionsCustmAPIClicked', {
app_identifier,
});
telemetry?.trackNodesPanel('nodeCreateList.onActionsCustmAPIClicked', { app_identifier });
nodeCreatorStore.onActionsCustomAPIClicked({ app_identifier });
}
// Anonymous component to handle triggers and actions rendering order

View file

@ -23,7 +23,6 @@ import ItemsRenderer from '../Renderers/ItemsRenderer.vue';
import CategorizedItemsRenderer from '../Renderers/CategorizedItemsRenderer.vue';
import NoResults from '../Panel/NoResults.vue';
import { useI18n } from '@/composables/useI18n';
import { useTelemetry } from '@/composables/useTelemetry';
import { getNodeIcon, getNodeIconColor, getNodeIconUrl } from '@/utils/nodeTypesUtils';
import { useUIStore } from '@/stores/ui.store';
@ -36,11 +35,10 @@ const emit = defineEmits<{
}>();
const i18n = useI18n();
const telemetry = useTelemetry();
const uiStore = useUIStore();
const rootStore = useRootStore();
const { mergedNodes, actions } = useNodeCreatorStore();
const { mergedNodes, actions, onSubcategorySelected } = useNodeCreatorStore();
const { pushViewStack, popViewStack } = useViewStacks();
const { registerKeyHook } = useKeyboardNavigation();
@ -83,7 +81,7 @@ function onSelected(item: INodeCreateElement) {
sections: item.properties.sections,
});
telemetry.trackNodesPanel('nodeCreateList.onSubcategorySelected', {
onSubcategorySelected({
subcategory: item.key,
});
}
@ -153,9 +151,6 @@ function onSelected(item: INodeCreateElement) {
if (item.type === 'link') {
window.open(item.properties.url, '_blank');
telemetry.trackNodesPanel('nodeCreateList.onLinkSelected', {
link: item.properties.url,
});
}
}

View file

@ -17,12 +17,15 @@ import SearchBar from './SearchBar.vue';
import ActionsRenderer from '../Modes/ActionsMode.vue';
import NodesRenderer from '../Modes/NodesMode.vue';
import { useI18n } from '@/composables/useI18n';
import { useDebounce } from '@/composables/useDebounce';
const i18n = useI18n();
const { callDebounced } = useDebounce();
const { mergedNodes } = useNodeCreatorStore();
const { pushViewStack, popViewStack, updateCurrentViewStack } = useViewStacks();
const { setActiveItemIndex, attachKeydownEvent, detachKeydownEvent } = useKeyboardNavigation();
const nodeCreatorStore = useNodeCreatorStore();
const activeViewStack = computed(() => useViewStacks().activeViewStack);
@ -55,6 +58,19 @@ function onSearch(value: string) {
if (activeViewStack.value.uuid) {
updateCurrentViewStack({ search: value });
void setActiveItemIndex(getDefaultActiveIndex(value));
if (value.length) {
callDebounced(
nodeCreatorStore.onNodeFilterChanged,
{ trailing: true, debounceTime: 2000 },
{
newValue: value,
filteredNodes: activeViewStack.value.items ?? [],
filterMode: activeViewStack.value.rootView ?? 'Regular',
subcategory: activeViewStack.value.subcategory,
title: activeViewStack.value.title,
},
);
}
}
}
@ -299,6 +315,7 @@ function onBackButton() {
margin-top: var(--spacing-4xs);
font-size: var(--font-size-s);
line-height: 19px;
color: var(--color-text-base);
font-weight: var(--font-weight-regular);
}

View file

@ -8,7 +8,7 @@ import { useKeyboardNavigation } from '../composables/useKeyboardNavigation';
import { useViewStacks } from '../composables/useViewStacks';
import ItemsRenderer from './ItemsRenderer.vue';
import CategoryItem from '../ItemTypes/CategoryItem.vue';
import { useTelemetry } from '@/composables/useTelemetry';
import { useNodeCreatorStore } from '@/stores/nodeCreator.store';
export interface Props {
elements: INodeCreateElement[];
@ -24,10 +24,10 @@ const props = withDefaults(defineProps<Props>(), {
elements: () => [],
});
const telemetry = useTelemetry();
const { popViewStack } = useViewStacks();
const { registerKeyHook } = useKeyboardNavigation();
const { workflowId } = useWorkflowsStore();
const nodeCreatorStore = useNodeCreatorStore();
const activeItemId = computed(() => useKeyboardNavigation()?.activeItemId);
const actionCount = computed(() => props.elements.filter(({ type }) => type === 'action').length);
@ -38,10 +38,11 @@ function toggleExpanded() {
}
function setExpanded(isExpanded: boolean) {
const prev = expanded.value;
expanded.value = isExpanded;
if (expanded.value) {
telemetry.trackNodesPanel('nodeCreateList.onCategoryExpanded', {
if (expanded.value && !prev) {
nodeCreatorStore.onCategoryExpanded({
category_name: props.category,
workflow_id: workflowId,
});

View file

@ -332,7 +332,11 @@ export const useActions = () => {
return storeWatcher;
}
function trackActionSelected(action: IUpdateInformation, telemetry: Telemetry, rootView: string) {
function trackActionSelected(
action: IUpdateInformation,
_telemetry: Telemetry,
rootView: string,
) {
const payload = {
node_type: action.key,
action: action.name,
@ -340,7 +344,7 @@ export const useActions = () => {
resource: (action.value as INodeParameters).resource || '',
};
void useExternalHooks().run('nodeCreateList.addAction', payload);
telemetry?.trackNodesPanel('nodeCreateList.addAction', payload);
useNodeCreatorStore().onAddActions(payload);
}
return {

View file

@ -84,7 +84,6 @@ import type {
INodeTypeDescription,
INodeTypeNameVersion,
IPinData,
ITelemetryTrackProperties,
IWorkflowBase,
NodeInputConnections,
NodeParameterValueType,
@ -733,25 +732,22 @@ export function useCanvasOperations({ router }: { router: ReturnType<typeof useR
}
function trackAddStickyNoteNode() {
telemetry.trackNodesPanel('nodeView.addSticky', {
telemetry.track('User inserted workflow note', {
workflow_id: workflowsStore.workflowId,
});
}
function trackAddDefaultNode(nodeData: INodeUi, options: AddNodeOptions) {
const trackProperties: ITelemetryTrackProperties = {
nodeCreatorStore.onNodeAddedToCanvas({
node_type: nodeData.type,
node_version: nodeData.typeVersion,
is_auto_add: options.isAutoAdd,
workflow_id: workflowsStore.workflowId,
drag_and_drop: options.dragAndDrop,
};
if (uiStore.lastInteractedWithNode) {
trackProperties.input_node_type = uiStore.lastInteractedWithNode.type;
}
telemetry.trackNodesPanel('nodeView.addNodeButton', trackProperties);
input_node_type: uiStore.lastInteractedWithNode
? uiStore.lastInteractedWithNode.type
: undefined,
});
}
/**

View file

@ -1,30 +0,0 @@
export const nodesPanelSession = {
pushRef: '',
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.pushRef,
},
};
};
export const hooksResetNodesPanelSession = () => {
nodesPanelSession.pushRef = `nodes_panel_session_${new Date().valueOf()}`;
nodesPanelSession.data = {
nodeFilter: '',
resultsNodes: [],
filterMode: 'Regular',
};
};

View file

@ -1,2 +1 @@
export * from './hooksAddFakeDoorFeatures';
export * from './hooksNodesPanel';

View file

@ -3,8 +3,8 @@ import type { ITelemetrySettings } from '@n8n/api-types';
import type { ITelemetryTrackProperties, IDataObject } from 'n8n-workflow';
import type { RouteLocation } from 'vue-router';
import type { INodeCreateElement, IUpdateInformation } from '@/Interface';
import type { IUserNodesPanelSession, RudderStack } from './telemetry.types';
import type { IUpdateInformation } from '@/Interface';
import type { RudderStack } from './telemetry.types';
import {
APPEND_ATTRIBUTION_DEFAULT_PATH,
MICROSOFT_TEAMS_NODE_TYPE,
@ -26,15 +26,6 @@ export class Telemetry {
return window.rudderanalytics;
}
private userNodesPanelSession: IUserNodesPanelSession = {
pushRef: '',
data: {
nodeFilter: '',
resultsNodes: [],
filterMode: 'Regular',
},
};
constructor() {
this.pageEventQueue = [];
this.previousPath = '';
@ -200,78 +191,6 @@ export class Telemetry {
}
}
trackNodesPanel(event: string, properties: IDataObject = {}) {
if (this.rudderStack) {
properties.nodes_panel_session_id = this.userNodesPanelSession.pushRef;
switch (event) {
case 'nodeView.createNodeActiveChanged':
if (properties.createNodeActive !== false) {
this.resetNodesPanelSession();
properties.nodes_panel_session_id = this.userNodesPanelSession.pushRef;
this.track('User opened nodes panel', properties);
}
break;
case 'nodeCreateList.destroyed':
if (
this.userNodesPanelSession.data.nodeFilter.length > 0 &&
this.userNodesPanelSession.data.nodeFilter !== ''
) {
this.track('User entered nodes panel search term', this.generateNodesPanelEvent());
}
break;
case 'nodeCreateList.nodeFilterChanged':
if (
(properties.newValue as string).length === 0 &&
this.userNodesPanelSession.data.nodeFilter.length > 0
) {
this.track('User entered nodes panel search term', this.generateNodesPanelEvent());
}
if (
(properties.newValue as string).length > ((properties.oldValue as string) || '').length
) {
this.userNodesPanelSession.data.nodeFilter = properties.newValue as string;
this.userNodesPanelSession.data.resultsNodes = (
(properties.filteredNodes || []) as INodeCreateElement[]
).map((node: INodeCreateElement) => node.key);
}
break;
case 'nodeCreateList.onCategoryExpanded':
properties.is_subcategory = false;
properties.nodes_panel_session_id = this.userNodesPanelSession.pushRef;
this.track('User viewed node category', properties);
break;
case 'nodeCreateList.onViewActions':
properties.nodes_panel_session_id = this.userNodesPanelSession.pushRef;
this.track('User viewed node actions', properties);
break;
case 'nodeCreateList.onActionsCustmAPIClicked':
properties.nodes_panel_session_id = this.userNodesPanelSession.pushRef;
this.track('User clicked custom API from node actions', properties);
break;
case 'nodeCreateList.addAction':
properties.nodes_panel_session_id = this.userNodesPanelSession.pushRef;
this.track('User added action', properties);
break;
case 'nodeCreateList.onSubcategorySelected':
properties.category_name = properties.subcategory;
properties.is_subcategory = true;
properties.nodes_panel_session_id = this.userNodesPanelSession.pushRef;
delete properties.selected;
this.track('User viewed node category', properties);
break;
case 'nodeView.addNodeButton':
this.track('User added node to workflow canvas', properties, { withPostHog: true });
break;
case 'nodeView.addSticky':
this.track('User inserted workflow note', properties);
break;
default:
break;
}
}
}
// We currently do not support tracking directly from within node implementation
// so we are using this method as centralized way to track node parameters changes
trackNodeParametersValuesChange(nodeType: string, change: IUpdateInformation) {
@ -295,24 +214,6 @@ export class Telemetry {
}
}
private resetNodesPanelSession() {
this.userNodesPanelSession.pushRef = `nodes_panel_session_${new Date().valueOf()}`;
this.userNodesPanelSession.data = {
nodeFilter: '',
resultsNodes: [],
filterMode: 'All',
};
}
private generateNodesPanelEvent() {
return {
search_string: this.userNodesPanelSession.data.nodeFilter,
results_count: this.userNodesPanelSession.data.resultsNodes.length,
filter_mode: this.userNodesPanelSession.data.filterMode,
nodes_panel_session_id: this.userNodesPanelSession.pushRef,
};
}
private initRudderStack(key: string, url: string, options: IDataObject) {
window.rudderanalytics = window.rudderanalytics || [];
if (!this.rudderStack) {

View file

@ -0,0 +1,297 @@
import { createPinia, setActivePinia } from 'pinia';
import { useNodeCreatorStore } from './nodeCreator.store';
import { useTelemetry } from '@/composables/useTelemetry';
import { CUSTOM_API_CALL_KEY, REGULAR_NODE_CREATOR_VIEW } from '@/constants';
import type { INodeCreateElement } from '@/Interface';
const workflow_id = 'workflow-id';
const category_name = 'category-name';
const source = 'source';
const mode = 'mode';
const now = 1717602004819;
const now1 = 1718602004819;
const node_type = 'node-type';
const node_version = 1;
const input_node_type = 'input-node-type';
const action = 'action';
const source_mode = 'source-mode';
const resource = 'resource';
const actions = ['action1'];
vi.mock('@/composables/useTelemetry', () => {
const track = vi.fn();
return {
useTelemetry: () => {
return {
track,
};
},
};
});
describe('useNodeCreatorStore', () => {
let nodeCreatorStore: ReturnType<typeof useNodeCreatorStore>;
beforeEach(() => {
vi.useFakeTimers();
vi.restoreAllMocks();
setActivePinia(createPinia());
nodeCreatorStore = useNodeCreatorStore();
vi.setSystemTime(now);
});
it('tracks when node creator is opened', () => {
nodeCreatorStore.onCreatorOpened({
source,
mode,
workflow_id,
});
expect(useTelemetry().track).toHaveBeenCalledWith(
'User opened nodes panel',
{
mode,
source,
nodes_panel_session_id: getSessionId(now),
workflow_id,
},
{
withPostHog: false,
},
);
});
it('resets session id every time node creator is opened', () => {
nodeCreatorStore.onCreatorOpened({
source,
mode,
workflow_id,
});
expect(useTelemetry().track).toHaveBeenCalledWith(
'User opened nodes panel',
{
mode,
source,
nodes_panel_session_id: getSessionId(now),
workflow_id,
},
{
withPostHog: false,
},
);
vi.setSystemTime(now1);
nodeCreatorStore.onCreatorOpened({
source,
mode,
workflow_id,
});
expect(useTelemetry().track).toHaveBeenCalledWith(
'User opened nodes panel',
{
mode,
source,
nodes_panel_session_id: getSessionId(now1),
workflow_id,
},
{
withPostHog: false,
},
);
});
it('tracks event on category expanded', () => {
nodeCreatorStore.onCreatorOpened({
source,
mode,
workflow_id,
});
nodeCreatorStore.onCategoryExpanded({ workflow_id, category_name });
expect(useTelemetry().track).toHaveBeenCalledWith(
'User viewed node category',
{
category_name,
is_subcategory: false,
nodes_panel_session_id: getSessionId(now),
workflow_id,
},
{
withPostHog: false,
},
);
});
it('tracks event when node is added to canvas', () => {
nodeCreatorStore.onCreatorOpened({
source,
mode,
workflow_id,
});
nodeCreatorStore.onNodeAddedToCanvas({
node_type,
node_version,
is_auto_add: true,
workflow_id,
drag_and_drop: true,
input_node_type,
});
expect(useTelemetry().track).toHaveBeenCalledWith(
'User added node to workflow canvas',
{
node_type,
node_version,
is_auto_add: true,
drag_and_drop: true,
input_node_type,
nodes_panel_session_id: getSessionId(now),
workflow_id,
},
{
withPostHog: true,
},
);
});
it('tracks event when action is added', () => {
nodeCreatorStore.onCreatorOpened({
source,
mode,
workflow_id,
});
nodeCreatorStore.onAddActions({
node_type,
action,
source_mode,
resource,
});
expect(useTelemetry().track).toHaveBeenCalledWith(
'User added action',
{
node_type,
action,
source_mode,
resource,
nodes_panel_session_id: getSessionId(now),
},
{
withPostHog: false,
},
);
});
it('tracks when custom api action is clicked', () => {
nodeCreatorStore.onCreatorOpened({
source,
mode,
workflow_id,
});
nodeCreatorStore.onActionsCustomAPIClicked({
app_identifier: node_type,
});
expect(useTelemetry().track).toHaveBeenCalledWith(
'User clicked custom API from node actions',
{
app_identifier: node_type,
nodes_panel_session_id: getSessionId(now),
},
{
withPostHog: false,
},
);
});
it('tracks when action is viewed', () => {
nodeCreatorStore.onCreatorOpened({
source,
mode,
workflow_id,
});
nodeCreatorStore.onViewActions({
app_identifier: node_type,
actions,
regular_action_count: 1,
trigger_action_count: 2,
});
expect(useTelemetry().track).toHaveBeenCalledWith(
'User viewed node actions',
{
app_identifier: node_type,
actions,
regular_action_count: 1,
trigger_action_count: 2,
nodes_panel_session_id: getSessionId(now),
},
{
withPostHog: false,
},
);
});
it('tracks when search filter is updated, ignoring custom actions in count', () => {
const newValue = 'new-value';
const subcategory = 'subcategory';
const title = 'title';
const mockTrigger = {
key: 'n8n-node.exampleTrigger',
properties: {
name: 'n8n-node.exampleTrigge',
displayName: 'Example Trigger',
},
} as INodeCreateElement;
const mockCustom = {
key: 'action',
properties: {
actionKey: CUSTOM_API_CALL_KEY,
},
} as INodeCreateElement;
const mockRegular = {
key: 'n8n-node.example',
properties: {},
} as INodeCreateElement;
nodeCreatorStore.onCreatorOpened({
source,
mode,
workflow_id,
});
nodeCreatorStore.onNodeFilterChanged({
newValue,
filteredNodes: [mockCustom, mockRegular, mockTrigger],
filterMode: REGULAR_NODE_CREATOR_VIEW,
subcategory,
title,
});
expect(useTelemetry().track).toHaveBeenCalledWith(
'User entered nodes panel search term',
{
search_string: newValue,
filter_mode: 'regular',
category_name: subcategory,
results_count: 2,
trigger_count: 1,
regular_count: 1,
nodes_panel_session_id: getSessionId(now),
title,
},
{
withPostHog: false,
},
);
});
});
function getSessionId(time: number) {
return `nodes_panel_session_${time}`;
}

View file

@ -1,6 +1,8 @@
import { defineStore } from 'pinia';
import {
AI_NODE_CREATOR_VIEW,
AI_OTHERS_NODE_CREATOR_VIEW,
CUSTOM_API_CALL_KEY,
NODE_CREATOR_OPEN_SOURCES,
REGULAR_NODE_CREATOR_VIEW,
STORES,
@ -12,17 +14,17 @@ import type {
SimplifiedNodeType,
ActionsRecord,
ToggleNodeCreatorOptions,
INodeCreateElement,
} from '@/Interface';
import { computed, ref } from 'vue';
import { transformNodeType } from '@/components/Node/NodeCreator/utils';
import type { INodeInputConfiguration } from 'n8n-workflow';
import type { IDataObject, INodeInputConfiguration, NodeParameterValueType } from 'n8n-workflow';
import { NodeConnectionType, nodeConnectionTypes, NodeHelpers } from 'n8n-workflow';
import { useWorkflowsStore } from '@/stores/workflows.store';
import { useUIStore } from '@/stores/ui.store';
import { useNDVStore } from '@/stores/ndv.store';
import { useExternalHooks } from '@/composables/useExternalHooks';
import { useTelemetry } from '@/composables/useTelemetry';
import { useViewStacks } from '@/components/Node/NodeCreator/composables/useViewStacks';
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
import {
@ -33,15 +35,15 @@ import type { Connection } from '@vue-flow/core';
import { CanvasConnectionMode } from '@/types';
import { isVueFlowConnection } from '@/utils/typeGuards';
import type { PartialBy } from '@/utils/typeHelpers';
import { useTelemetry } from '@/composables/useTelemetry';
export const useNodeCreatorStore = defineStore(STORES.NODE_CREATOR, () => {
const workflowsStore = useWorkflowsStore();
const ndvStore = useNDVStore();
const uiStore = useUIStore();
const nodeTypesStore = useNodeTypesStore();
const externalHooks = useExternalHooks();
const telemetry = useTelemetry();
const externalHooks = useExternalHooks();
const selectedView = ref<NodeFilterType>(TRIGGER_NODE_CREATOR_VIEW);
const mergedNodes = ref<SimplifiedNodeType[]>([]);
@ -50,6 +52,10 @@ export const useNodeCreatorStore = defineStore(STORES.NODE_CREATOR, () => {
const showScrim = ref(false);
const openSource = ref<NodeCreatorOpenSource>('');
const isCreateNodeActive = ref<boolean>(false);
const nodePanelSessionId = ref<string>('');
const allNodeCreatorNodes = computed(() =>
Object.values(mergedNodes.value).map((i) => transformNodeType(i)),
);
@ -115,7 +121,7 @@ export const useNodeCreatorStore = defineStore(STORES.NODE_CREATOR, () => {
createNodeActive,
nodeCreatorView,
}: ToggleNodeCreatorOptions) {
if (createNodeActive === uiStore.isCreateNodeActive) {
if (createNodeActive === isCreateNodeActive.value) {
return;
}
@ -128,54 +134,24 @@ export const useNodeCreatorStore = defineStore(STORES.NODE_CREATOR, () => {
// Default to the trigger tab in node creator if there's no trigger node yet
setSelectedView(nodeCreatorView);
let mode;
switch (selectedView.value) {
case AI_NODE_CREATOR_VIEW:
mode = 'ai';
break;
case REGULAR_NODE_CREATOR_VIEW:
mode = 'regular';
break;
default:
mode = 'regular';
}
uiStore.isCreateNodeActive = createNodeActive;
isCreateNodeActive.value = createNodeActive;
if (createNodeActive && source) {
setOpenSource(source);
}
void externalHooks.run('nodeView.createNodeActiveChanged', {
source,
mode,
mode: getMode(nodeCreatorView),
createNodeActive,
});
trackNodesPanelActiveChanged({
if (createNodeActive) {
onCreatorOpened({
source,
mode,
createNodeActive,
workflowId: workflowsStore.workflowId,
mode: getMode(nodeCreatorView),
workflow_id: workflowsStore.workflowId,
});
}
function trackNodesPanelActiveChanged({
source,
mode,
createNodeActive,
workflowId,
}: {
source?: string;
mode?: string;
createNodeActive?: boolean;
workflowId?: string;
}) {
telemetry.trackNodesPanel('nodeView.createNodeActiveChanged', {
source,
mode,
createNodeActive,
workflow_id: workflowId,
});
}
function openNodeCreatorForConnectingNode({
@ -264,7 +240,150 @@ export const useNodeCreatorStore = defineStore(STORES.NODE_CREATOR, () => {
return filter;
}
function resetNodesPanelSession() {
nodePanelSessionId.value = `nodes_panel_session_${new Date().valueOf()}`;
}
function trackNodeCreatorEvent(event: string, properties: IDataObject = {}, withPostHog = false) {
telemetry.track(
event,
{
...properties,
nodes_panel_session_id: nodePanelSessionId.value,
},
{
withPostHog,
},
);
}
function onCreatorOpened({
source,
mode,
workflow_id,
}: {
source?: string;
mode: string;
workflow_id?: string;
}) {
resetNodesPanelSession();
trackNodeCreatorEvent('User opened nodes panel', {
source,
mode,
workflow_id,
});
}
function onNodeFilterChanged({
newValue,
filteredNodes,
filterMode,
subcategory,
title,
}: {
newValue: string;
filteredNodes: INodeCreateElement[];
filterMode: NodeFilterType;
subcategory?: string;
title?: string;
}) {
if (!newValue.length) {
return;
}
const { results_count, trigger_count, regular_count } = filteredNodes.reduce(
(accu, node) => {
if (!('properties' in node)) {
return accu;
}
const isCustomAction =
'actionKey' in node.properties && node.properties.actionKey === CUSTOM_API_CALL_KEY;
if (isCustomAction) {
return accu;
}
const isTrigger = node.key.includes('Trigger');
return {
results_count: accu.results_count + 1,
trigger_count: accu.trigger_count + (isTrigger ? 1 : 0),
regular_count: accu.regular_count + (isTrigger ? 0 : 1),
};
},
{
results_count: 0,
trigger_count: 0,
regular_count: 0,
},
);
trackNodeCreatorEvent('User entered nodes panel search term', {
search_string: newValue,
filter_mode: getMode(filterMode),
category_name: subcategory,
results_count,
trigger_count,
regular_count,
title,
});
}
function onCategoryExpanded(properties: { category_name: string; workflow_id: string }) {
trackNodeCreatorEvent('User viewed node category', { ...properties, is_subcategory: false });
}
function onViewActions(properties: {
app_identifier: string;
actions: string[];
regular_action_count: number;
trigger_action_count: number;
}) {
trackNodeCreatorEvent('User viewed node actions', properties);
}
function onActionsCustomAPIClicked(properties: { app_identifier: string }) {
trackNodeCreatorEvent('User clicked custom API from node actions', properties);
}
function onAddActions(properties: {
node_type?: string;
action: string;
source_mode: string;
resource: NodeParameterValueType;
}) {
trackNodeCreatorEvent('User added action', properties);
}
function onSubcategorySelected(properties: { subcategory: string }) {
trackNodeCreatorEvent('User viewed node category', {
category_name: properties.subcategory,
is_subcategory: true,
});
}
function onNodeAddedToCanvas(properties: {
node_type: string;
node_version: number;
is_auto_add?: boolean;
workflow_id: string;
drag_and_drop?: boolean;
input_node_type?: string;
}) {
trackNodeCreatorEvent('User added node to workflow canvas', properties, true);
}
function getMode(mode: NodeFilterType): string {
if (mode === AI_NODE_CREATOR_VIEW || mode === AI_OTHERS_NODE_CREATOR_VIEW) {
return 'ai';
}
if (mode === TRIGGER_NODE_CREATOR_VIEW) {
return 'trigger';
}
return 'regular';
}
return {
isCreateNodeActive,
openSource,
selectedView,
showScrim,
@ -280,5 +399,13 @@ export const useNodeCreatorStore = defineStore(STORES.NODE_CREATOR, () => {
openNodeCreatorForConnectingNode,
openNodeCreatorForTriggerNodes,
allNodeCreatorNodes,
onCreatorOpened,
onNodeFilterChanged,
onCategoryExpanded,
onActionsCustomAPIClicked,
onViewActions,
onAddActions,
onSubcategorySelected,
onNodeAddedToCanvas,
};
});

View file

@ -192,7 +192,6 @@ export const useUIStore = defineStore(STORES.UI, () => {
const bannersHeight = ref<number>(0);
const bannerStack = ref<BannerName[]>([]);
const pendingNotificationsForViews = ref<{ [key in VIEWS]?: NotificationOptions[] }>({});
const isCreateNodeActive = ref<boolean>(false);
const appGridWidth = ref<number>(0);
@ -659,7 +658,6 @@ export const useUIStore = defineStore(STORES.UI, () => {
nodeViewMoveInProgress,
nodeViewInitialized,
addFirstStepOnLoad,
isCreateNodeActive,
sidebarMenuCollapsed,
fakeDoorFeatures,
bannerStack,

View file

@ -1450,7 +1450,7 @@ function selectNodes(ids: string[]) {
function onClickPane(position: CanvasNode['position']) {
lastClickPosition.value = [position.x, position.y];
uiStore.isCreateNodeActive = false;
nodeCreatorStore.isCreateNodeActive = false;
setNodeSelected();
}
@ -1643,7 +1643,7 @@ onBeforeUnmount(() => {
<Suspense>
<LazyNodeCreation
v-if="!isCanvasReadOnly"
:create-node-active="uiStore.isCreateNodeActive"
:create-node-active="nodeCreatorStore.isCreateNodeActive"
:node-view-scale="viewportTransform.zoom"
@toggle-node-creator="onToggleNodeCreator"
@add-nodes="onAddNodesAndConnections"

View file

@ -71,7 +71,6 @@ import type {
INodeInputConfiguration,
INodeTypeDescription,
ITaskData,
ITelemetryTrackProperties,
IWorkflowBase,
Workflow,
INodeOutputConfiguration,
@ -2398,24 +2397,19 @@ export default defineComponent({
this.uiStore.stateIsDirty = true;
if (nodeTypeName === STICKY_NODE_TYPE) {
this.$telemetry.trackNodesPanel('nodeView.addSticky', {
this.$telemetry.track('User inserted workflow note', {
workflow_id: this.workflowsStore.workflowId,
});
} else {
void this.externalHooks.run('nodeView.addNodeButton', { nodeTypeName });
const trackProperties: ITelemetryTrackProperties = {
this.nodeCreatorStore.onNodeAddedToCanvas({
node_type: nodeTypeName,
node_version: newNodeData.typeVersion,
is_auto_add: isAutoAdd,
workflow_id: this.workflowsStore.workflowId,
drag_and_drop: options.dragAndDrop,
};
if (lastSelectedNode) {
trackProperties.input_node_type = lastSelectedNode.type;
}
this.$telemetry.trackNodesPanel('nodeView.addNodeButton', trackProperties);
input_node_type: lastSelectedNode ? lastSelectedNode.type : undefined,
});
}
// Automatically deselect all nodes and select the current one and also active
@ -4242,12 +4236,13 @@ export default defineComponent({
mode,
createNodeActive,
});
this.$telemetry.trackNodesPanel('nodeView.createNodeActiveChanged', {
if (createNodeActive) {
this.nodeCreatorStore.onCreatorOpened({
source,
mode,
createNodeActive,
workflow_id: this.workflowsStore.workflowId,
});
}
},
async onAddNodes(
{ nodes, connections }: AddedNodesAndConnections,