feat(editor): Add telemetry for schema preview (no-changelog) (#13901)

This commit is contained in:
Elias Meire
2025-03-13 18:50:19 +01:00
committed by GitHub
parent dedcdbd314
commit c9be48ea20
5 changed files with 131 additions and 32 deletions

View File

@@ -31,6 +31,7 @@ const mockNode1 = createTestNode({
type: MANUAL_TRIGGER_NODE_TYPE, type: MANUAL_TRIGGER_NODE_TYPE,
typeVersion: 1, typeVersion: 1,
disabled: false, disabled: false,
credentials: undefined,
}); });
const mockNode2 = createTestNode({ const mockNode2 = createTestNode({
@@ -419,40 +420,111 @@ describe('VirtualSchema.vue', () => {
}); });
}); });
it('should handle drop event', async () => { describe('telemetry', () => {
const ndvStore = useNDVStore(); function dragDropPill(pill: HTMLElement) {
useWorkflowsStore().pinData({ const ndvStore = useNDVStore();
node: mockNode1, const reset = vi.spyOn(ndvStore, 'resetMappingTelemetry');
data: [{ json: { name: 'John', age: 22, hobbies: ['surfing', 'traveling'] } }], fireEvent(pill, new MouseEvent('mousedown', { bubbles: true }));
fireEvent(window, new MouseEvent('mousemove', { bubbles: true }));
expect(reset).toHaveBeenCalled();
vi.useFakeTimers({ toFake: ['setTimeout'] });
fireEvent(window, new MouseEvent('mouseup', { bubbles: true }));
vi.advanceTimersByTime(250);
vi.useRealTimers();
}
it('should track data pill drag and drop', async () => {
useWorkflowsStore().pinData({
node: mockNode1,
data: [{ json: { name: 'John', age: 22, hobbies: ['surfing', 'traveling'] } }],
});
const telemetry = useTelemetry();
const trackSpy = vi.spyOn(telemetry, 'track');
const { getAllByTestId } = renderComponent();
await waitFor(() => {
expect(getAllByTestId('run-data-schema-item')).toHaveLength(6);
});
const items = getAllByTestId('run-data-schema-item');
expect(items[0].className).toBe('schema-item draggable');
expect(items[0]).toHaveTextContent('nameJohn');
const pill = items[0].querySelector('.pill') as HTMLElement;
dragDropPill(pill);
await waitFor(() =>
expect(trackSpy).toHaveBeenCalledWith(
'User dragged data for mapping',
expect.objectContaining({
src_view: 'schema',
src_field_name: 'name',
src_field_nest_level: 0,
src_node_type: 'n8n-nodes-base.manualTrigger',
src_nodes_back: '1',
src_has_credential: false,
}),
{ withPostHog: true },
),
);
}); });
const telemetry = useTelemetry();
const trackSpy = vi.spyOn(telemetry, 'track');
const reset = vi.spyOn(ndvStore, 'resetMappingTelemetry');
const { getAllByTestId } = renderComponent();
await waitFor(() => { it('should track data pill drag and drop for schema preview', async () => {
expect(getAllByTestId('run-data-schema-item')).toHaveLength(6); useWorkflowsStore().pinData({
node: {
...mockNode2,
credentials: { myCredential: { id: 'myCredential', name: 'myCredential' } },
},
data: [],
});
const telemetry = useTelemetry();
const trackSpy = vi.spyOn(telemetry, 'track');
const posthogStore = usePostHog();
vi.spyOn(posthogStore, 'isFeatureEnabled').mockReturnValue(true);
const schemaPreviewStore = useSchemaPreviewStore();
vi.spyOn(schemaPreviewStore, 'getSchemaPreview').mockResolvedValue(
createResultOk({
type: 'object',
properties: {
account: {
type: 'object',
properties: {
id: {
type: 'string',
},
},
},
},
}),
);
const { getAllByTestId } = renderComponent({
props: {
nodes: [{ name: mockNode2.name, indicies: [], depth: 1 }],
},
});
await waitFor(() => {
expect(getAllByTestId('run-data-schema-item')).toHaveLength(2);
});
const pill = getAllByTestId('run-data-schema-item')[0].querySelector('.pill') as HTMLElement;
dragDropPill(pill);
await waitFor(() =>
expect(trackSpy).toHaveBeenCalledWith(
'User dragged data for mapping',
expect.objectContaining({
src_view: 'schema_preview',
src_has_credential: true,
}),
{ withPostHog: true },
),
);
}); });
const items = getAllByTestId('run-data-schema-item');
expect(items[0].className).toBe('schema-item draggable');
expect(items[0]).toHaveTextContent('nameJohn');
const pill = items[0].querySelector('.pill') as Element;
fireEvent(pill, new MouseEvent('mousedown', { bubbles: true }));
fireEvent(window, new MouseEvent('mousemove', { bubbles: true }));
expect(reset).toHaveBeenCalled();
fireEvent(window, new MouseEvent('mouseup', { bubbles: true }));
await waitFor(() =>
expect(trackSpy).toHaveBeenCalledWith(
'User dragged data for mapping',
expect.any(Object),
expect.any(Object),
),
);
}); });
it('should expand all nodes when searching', async () => { it('should expand all nodes when searching', async () => {

View File

@@ -32,6 +32,7 @@ import { useSchemaPreviewStore } from '@/stores/schemaPreview.store';
import { asyncComputed } from '@vueuse/core'; import { asyncComputed } from '@vueuse/core';
import { usePostHog } from '@/stores/posthog.store'; import { usePostHog } from '@/stores/posthog.store';
import { SCHEMA_PREVIEW_EXPERIMENT } from '@/constants'; import { SCHEMA_PREVIEW_EXPERIMENT } from '@/constants';
import { isEmpty } from '@/utils/typesUtils';
type Props = { type Props = {
nodes?: IConnectedNode[]; nodes?: IConnectedNode[];
@@ -244,6 +245,11 @@ const onDragStart = () => {
const onDragEnd = (el: HTMLElement) => { const onDragEnd = (el: HTMLElement) => {
setTimeout(() => { setTimeout(() => {
const mappingTelemetry = ndvStore.mappingTelemetry; const mappingTelemetry = ndvStore.mappingTelemetry;
const parentNode = nodesSchemas.value.find(({ node }) => node.name === el.dataset.nodeName);
const isPreview = parentNode?.preview ?? false;
const hasCredential = !isEmpty(parentNode?.node.credentials);
const telemetryPayload = { const telemetryPayload = {
src_node_type: el.dataset.nodeType, src_node_type: el.dataset.nodeType,
src_field_name: el.dataset.name ?? '', src_field_name: el.dataset.name ?? '',
@@ -251,7 +257,8 @@ const onDragEnd = (el: HTMLElement) => {
src_run_index: props.runIndex, src_run_index: props.runIndex,
src_runs_total: props.totalRuns, src_runs_total: props.totalRuns,
src_field_nest_level: el.dataset.level ?? 0, src_field_nest_level: el.dataset.level ?? 0,
src_view: 'schema', src_view: isPreview ? 'schema_preview' : 'schema',
src_has_credential: hasCredential,
src_element: el, src_element: el,
success: false, success: false,
...mappingTelemetry, ...mappingTelemetry,

View File

@@ -12,6 +12,7 @@ type Props = {
id: string; id: string;
icon: string; icon: string;
collapsable?: boolean; collapsable?: boolean;
nodeName?: string;
nodeType?: string; nodeType?: string;
highlight?: boolean; highlight?: boolean;
draggable?: boolean; draggable?: boolean;
@@ -21,6 +22,7 @@ type Props = {
}; };
const props = defineProps<Props>(); const props = defineProps<Props>();
const emit = defineEmits<{ const emit = defineEmits<{
click: []; click: [];
}>(); }>();
@@ -41,6 +43,7 @@ const emit = defineEmits<{
:data-nest-level="level" :data-nest-level="level"
:data-value="expression" :data-value="expression"
:data-node-type="nodeType" :data-node-type="nodeType"
:data-node-name="nodeName"
data-target="mappable" data-target="mappable"
class="pill" class="pill"
:class="{ :class="{

View File

@@ -200,6 +200,7 @@ exports[`VirtualSchema.vue > renders preview schema when enabled and available 1
data-depth="1" data-depth="1"
data-name="account" data-name="account"
data-nest-level="1" data-nest-level="1"
data-node-name="Manual Trigger"
data-node-type="n8n-nodes-base.manualTrigger" data-node-type="n8n-nodes-base.manualTrigger"
data-path=".account" data-path=".account"
data-target="mappable" data-target="mappable"
@@ -272,6 +273,7 @@ exports[`VirtualSchema.vue > renders preview schema when enabled and available 1
data-depth="1" data-depth="1"
data-name="id" data-name="id"
data-nest-level="2" data-nest-level="2"
data-node-name="Manual Trigger"
data-node-type="n8n-nodes-base.manualTrigger" data-node-type="n8n-nodes-base.manualTrigger"
data-path=".account.id" data-path=".account.id"
data-target="mappable" data-target="mappable"
@@ -525,6 +527,7 @@ exports[`VirtualSchema.vue > renders preview schema when enabled and available 1
data-depth="2" data-depth="2"
data-name="account" data-name="account"
data-nest-level="1" data-nest-level="1"
data-node-name="Set2"
data-node-type="n8n-nodes-base.set" data-node-type="n8n-nodes-base.set"
data-path=".account" data-path=".account"
data-target="mappable" data-target="mappable"
@@ -597,6 +600,7 @@ exports[`VirtualSchema.vue > renders preview schema when enabled and available 1
data-depth="2" data-depth="2"
data-name="id" data-name="id"
data-nest-level="2" data-nest-level="2"
data-node-name="Set2"
data-node-type="n8n-nodes-base.set" data-node-type="n8n-nodes-base.set"
data-path=".account.id" data-path=".account.id"
data-target="mappable" data-target="mappable"
@@ -882,6 +886,7 @@ exports[`VirtualSchema.vue > renders schema in output pane 1`] = `
data-depth="0" data-depth="0"
data-name="name" data-name="name"
data-nest-level="0" data-nest-level="0"
data-node-name=""
data-node-type="" data-node-type=""
data-path=".name" data-path=".name"
data-target="mappable" data-target="mappable"
@@ -959,6 +964,7 @@ exports[`VirtualSchema.vue > renders schema in output pane 1`] = `
data-depth="0" data-depth="0"
data-name="age" data-name="age"
data-nest-level="0" data-nest-level="0"
data-node-name=""
data-node-type="" data-node-type=""
data-path=".age" data-path=".age"
data-target="mappable" data-target="mappable"
@@ -1062,6 +1068,7 @@ exports[`VirtualSchema.vue > renders schema in output pane 1`] = `
data-depth="0" data-depth="0"
data-name="hobbies" data-name="hobbies"
data-nest-level="0" data-nest-level="0"
data-node-name=""
data-node-type="" data-node-type=""
data-path=".hobbies" data-path=".hobbies"
data-target="mappable" data-target="mappable"
@@ -1134,6 +1141,7 @@ exports[`VirtualSchema.vue > renders schema in output pane 1`] = `
data-depth="0" data-depth="0"
data-name="hobbies[0]" data-name="hobbies[0]"
data-nest-level="1" data-nest-level="1"
data-node-name=""
data-node-type="" data-node-type=""
data-path=".hobbies[0]" data-path=".hobbies[0]"
data-target="mappable" data-target="mappable"
@@ -1211,6 +1219,7 @@ exports[`VirtualSchema.vue > renders schema in output pane 1`] = `
data-depth="0" data-depth="0"
data-name="hobbies[1]" data-name="hobbies[1]"
data-nest-level="1" data-nest-level="1"
data-node-name=""
data-node-type="" data-node-type=""
data-path=".hobbies[1]" data-path=".hobbies[1]"
data-target="mappable" data-target="mappable"
@@ -1446,6 +1455,7 @@ exports[`VirtualSchema.vue > renders schema with spaces and dots 1`] = `
data-depth="1" data-depth="1"
data-name="hello world" data-name="hello world"
data-nest-level="1" data-nest-level="1"
data-node-name="Manual Trigger"
data-node-type="n8n-nodes-base.manualTrigger" data-node-type="n8n-nodes-base.manualTrigger"
data-path="['hello world']" data-path="['hello world']"
data-target="mappable" data-target="mappable"
@@ -1544,6 +1554,7 @@ exports[`VirtualSchema.vue > renders schema with spaces and dots 1`] = `
data-depth="1" data-depth="1"
data-name="hello world[0]" data-name="hello world[0]"
data-nest-level="2" data-nest-level="2"
data-node-name="Manual Trigger"
data-node-type="n8n-nodes-base.manualTrigger" data-node-type="n8n-nodes-base.manualTrigger"
data-path="['hello world'][0]" data-path="['hello world'][0]"
data-target="mappable" data-target="mappable"
@@ -1642,6 +1653,7 @@ exports[`VirtualSchema.vue > renders schema with spaces and dots 1`] = `
data-depth="1" data-depth="1"
data-name="test" data-name="test"
data-nest-level="3" data-nest-level="3"
data-node-name="Manual Trigger"
data-node-type="n8n-nodes-base.manualTrigger" data-node-type="n8n-nodes-base.manualTrigger"
data-path="['hello world'][0].test" data-path="['hello world'][0].test"
data-target="mappable" data-target="mappable"
@@ -1714,6 +1726,7 @@ exports[`VirtualSchema.vue > renders schema with spaces and dots 1`] = `
data-depth="1" data-depth="1"
data-name="more to think about" data-name="more to think about"
data-nest-level="4" data-nest-level="4"
data-node-name="Manual Trigger"
data-node-type="n8n-nodes-base.manualTrigger" data-node-type="n8n-nodes-base.manualTrigger"
data-path="['hello world'][0].test['more to think about']" data-path="['hello world'][0].test['more to think about']"
data-target="mappable" data-target="mappable"
@@ -1791,6 +1804,7 @@ exports[`VirtualSchema.vue > renders schema with spaces and dots 1`] = `
data-depth="1" data-depth="1"
data-name="test.how" data-name="test.how"
data-nest-level="3" data-nest-level="3"
data-node-name="Manual Trigger"
data-node-type="n8n-nodes-base.manualTrigger" data-node-type="n8n-nodes-base.manualTrigger"
data-path="['hello world'][0]['test.how']" data-path="['hello world'][0]['test.how']"
data-target="mappable" data-target="mappable"

View File

@@ -245,6 +245,7 @@ export type RenderItem = {
id: string; id: string;
icon: string; icon: string;
collapsable?: boolean; collapsable?: boolean;
nodeName?: string;
nodeType?: INodeUi['type']; nodeType?: INodeUi['type'];
preview?: boolean; preview?: boolean;
type: 'item'; type: 'item';
@@ -371,6 +372,7 @@ export const useFlattenSchema = () => {
icon: getIconBySchemaType(schema.type), icon: getIconBySchemaType(schema.type),
id, id,
collapsable: true, collapsable: true,
nodeName: node.name,
nodeType: node.type, nodeType: node.type,
type: 'item', type: 'item',
preview, preview,
@@ -409,6 +411,7 @@ export const useFlattenSchema = () => {
icon: getIconBySchemaType(schema.type), icon: getIconBySchemaType(schema.type),
collapsable: false, collapsable: false,
nodeType: node.type, nodeType: node.type,
nodeName: node.name,
type: 'item', type: 'item',
preview, preview,
}, },