mirror of
https://github.com/n8n-io/n8n.git
synced 2024-12-25 04:34:06 -08:00
fix: Bring back nodes panel telemetry events (#11456)
This commit is contained in:
parent
529d4fc3ef
commit
130c942f63
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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',
|
||||
};
|
||||
};
|
|
@ -1,2 +1 @@
|
|||
export * from './hooksAddFakeDoorFeatures';
|
||||
export * from './hooksNodesPanel';
|
||||
|
|
|
@ -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) {
|
||||
|
|
297
packages/editor-ui/src/stores/nodeCreator.store.test.ts
Normal file
297
packages/editor-ui/src/stores/nodeCreator.store.test.ts
Normal 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}`;
|
||||
}
|
|
@ -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,
|
||||
};
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Reference in a new issue