From c9be48ea20365a3f89bfb2b5a4bc2dfae77ff19a Mon Sep 17 00:00:00 2001 From: Elias Meire Date: Thu, 13 Mar 2025 18:50:19 +0100 Subject: [PATCH] feat(editor): Add telemetry for schema preview (no-changelog) (#13901) --- .../src/components/VirtualSchema.test.ts | 134 ++++++++++++++---- .../src/components/VirtualSchema.vue | 9 +- .../src/components/VirtualSchemaItem.vue | 3 + .../__snapshots__/VirtualSchema.test.ts.snap | 14 ++ .../src/composables/useDataSchema.ts | 3 + 5 files changed, 131 insertions(+), 32 deletions(-) diff --git a/packages/frontend/editor-ui/src/components/VirtualSchema.test.ts b/packages/frontend/editor-ui/src/components/VirtualSchema.test.ts index a81bec4808..b0e65053ce 100644 --- a/packages/frontend/editor-ui/src/components/VirtualSchema.test.ts +++ b/packages/frontend/editor-ui/src/components/VirtualSchema.test.ts @@ -31,6 +31,7 @@ const mockNode1 = createTestNode({ type: MANUAL_TRIGGER_NODE_TYPE, typeVersion: 1, disabled: false, + credentials: undefined, }); const mockNode2 = createTestNode({ @@ -419,40 +420,111 @@ describe('VirtualSchema.vue', () => { }); }); - it('should handle drop event', async () => { - const ndvStore = useNDVStore(); - useWorkflowsStore().pinData({ - node: mockNode1, - data: [{ json: { name: 'John', age: 22, hobbies: ['surfing', 'traveling'] } }], + describe('telemetry', () => { + function dragDropPill(pill: HTMLElement) { + const ndvStore = useNDVStore(); + const reset = vi.spyOn(ndvStore, 'resetMappingTelemetry'); + 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(() => { - expect(getAllByTestId('run-data-schema-item')).toHaveLength(6); + it('should track data pill drag and drop for schema preview', async () => { + 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 () => { diff --git a/packages/frontend/editor-ui/src/components/VirtualSchema.vue b/packages/frontend/editor-ui/src/components/VirtualSchema.vue index 7ef4020dcf..e9a05c30bb 100644 --- a/packages/frontend/editor-ui/src/components/VirtualSchema.vue +++ b/packages/frontend/editor-ui/src/components/VirtualSchema.vue @@ -32,6 +32,7 @@ import { useSchemaPreviewStore } from '@/stores/schemaPreview.store'; import { asyncComputed } from '@vueuse/core'; import { usePostHog } from '@/stores/posthog.store'; import { SCHEMA_PREVIEW_EXPERIMENT } from '@/constants'; +import { isEmpty } from '@/utils/typesUtils'; type Props = { nodes?: IConnectedNode[]; @@ -244,6 +245,11 @@ const onDragStart = () => { const onDragEnd = (el: HTMLElement) => { setTimeout(() => { 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 = { src_node_type: el.dataset.nodeType, src_field_name: el.dataset.name ?? '', @@ -251,7 +257,8 @@ const onDragEnd = (el: HTMLElement) => { src_run_index: props.runIndex, src_runs_total: props.totalRuns, src_field_nest_level: el.dataset.level ?? 0, - src_view: 'schema', + src_view: isPreview ? 'schema_preview' : 'schema', + src_has_credential: hasCredential, src_element: el, success: false, ...mappingTelemetry, diff --git a/packages/frontend/editor-ui/src/components/VirtualSchemaItem.vue b/packages/frontend/editor-ui/src/components/VirtualSchemaItem.vue index 0b9ee3242a..c160a56a41 100644 --- a/packages/frontend/editor-ui/src/components/VirtualSchemaItem.vue +++ b/packages/frontend/editor-ui/src/components/VirtualSchemaItem.vue @@ -12,6 +12,7 @@ type Props = { id: string; icon: string; collapsable?: boolean; + nodeName?: string; nodeType?: string; highlight?: boolean; draggable?: boolean; @@ -21,6 +22,7 @@ type Props = { }; const props = defineProps(); + const emit = defineEmits<{ click: []; }>(); @@ -41,6 +43,7 @@ const emit = defineEmits<{ :data-nest-level="level" :data-value="expression" :data-node-type="nodeType" + :data-node-name="nodeName" data-target="mappable" class="pill" :class="{ diff --git a/packages/frontend/editor-ui/src/components/__snapshots__/VirtualSchema.test.ts.snap b/packages/frontend/editor-ui/src/components/__snapshots__/VirtualSchema.test.ts.snap index 8f72865bea..7e5e801189 100644 --- a/packages/frontend/editor-ui/src/components/__snapshots__/VirtualSchema.test.ts.snap +++ b/packages/frontend/editor-ui/src/components/__snapshots__/VirtualSchema.test.ts.snap @@ -200,6 +200,7 @@ exports[`VirtualSchema.vue > renders preview schema when enabled and available 1 data-depth="1" data-name="account" data-nest-level="1" + data-node-name="Manual Trigger" data-node-type="n8n-nodes-base.manualTrigger" data-path=".account" data-target="mappable" @@ -272,6 +273,7 @@ exports[`VirtualSchema.vue > renders preview schema when enabled and available 1 data-depth="1" data-name="id" data-nest-level="2" + data-node-name="Manual Trigger" data-node-type="n8n-nodes-base.manualTrigger" data-path=".account.id" data-target="mappable" @@ -525,6 +527,7 @@ exports[`VirtualSchema.vue > renders preview schema when enabled and available 1 data-depth="2" data-name="account" data-nest-level="1" + data-node-name="Set2" data-node-type="n8n-nodes-base.set" data-path=".account" data-target="mappable" @@ -597,6 +600,7 @@ exports[`VirtualSchema.vue > renders preview schema when enabled and available 1 data-depth="2" data-name="id" data-nest-level="2" + data-node-name="Set2" data-node-type="n8n-nodes-base.set" data-path=".account.id" data-target="mappable" @@ -882,6 +886,7 @@ exports[`VirtualSchema.vue > renders schema in output pane 1`] = ` data-depth="0" data-name="name" data-nest-level="0" + data-node-name="" data-node-type="" data-path=".name" data-target="mappable" @@ -959,6 +964,7 @@ exports[`VirtualSchema.vue > renders schema in output pane 1`] = ` data-depth="0" data-name="age" data-nest-level="0" + data-node-name="" data-node-type="" data-path=".age" data-target="mappable" @@ -1062,6 +1068,7 @@ exports[`VirtualSchema.vue > renders schema in output pane 1`] = ` data-depth="0" data-name="hobbies" data-nest-level="0" + data-node-name="" data-node-type="" data-path=".hobbies" data-target="mappable" @@ -1134,6 +1141,7 @@ exports[`VirtualSchema.vue > renders schema in output pane 1`] = ` data-depth="0" data-name="hobbies[0]" data-nest-level="1" + data-node-name="" data-node-type="" data-path=".hobbies[0]" data-target="mappable" @@ -1211,6 +1219,7 @@ exports[`VirtualSchema.vue > renders schema in output pane 1`] = ` data-depth="0" data-name="hobbies[1]" data-nest-level="1" + data-node-name="" data-node-type="" data-path=".hobbies[1]" data-target="mappable" @@ -1446,6 +1455,7 @@ exports[`VirtualSchema.vue > renders schema with spaces and dots 1`] = ` data-depth="1" data-name="hello world" data-nest-level="1" + data-node-name="Manual Trigger" data-node-type="n8n-nodes-base.manualTrigger" data-path="['hello world']" data-target="mappable" @@ -1544,6 +1554,7 @@ exports[`VirtualSchema.vue > renders schema with spaces and dots 1`] = ` data-depth="1" data-name="hello world[0]" data-nest-level="2" + data-node-name="Manual Trigger" data-node-type="n8n-nodes-base.manualTrigger" data-path="['hello world'][0]" data-target="mappable" @@ -1642,6 +1653,7 @@ exports[`VirtualSchema.vue > renders schema with spaces and dots 1`] = ` data-depth="1" data-name="test" data-nest-level="3" + data-node-name="Manual Trigger" data-node-type="n8n-nodes-base.manualTrigger" data-path="['hello world'][0].test" data-target="mappable" @@ -1714,6 +1726,7 @@ exports[`VirtualSchema.vue > renders schema with spaces and dots 1`] = ` data-depth="1" data-name="more to think about" data-nest-level="4" + data-node-name="Manual Trigger" data-node-type="n8n-nodes-base.manualTrigger" data-path="['hello world'][0].test['more to think about']" data-target="mappable" @@ -1791,6 +1804,7 @@ exports[`VirtualSchema.vue > renders schema with spaces and dots 1`] = ` data-depth="1" data-name="test.how" data-nest-level="3" + data-node-name="Manual Trigger" data-node-type="n8n-nodes-base.manualTrigger" data-path="['hello world'][0]['test.how']" data-target="mappable" diff --git a/packages/frontend/editor-ui/src/composables/useDataSchema.ts b/packages/frontend/editor-ui/src/composables/useDataSchema.ts index 20ab524fb0..6c2c14239e 100644 --- a/packages/frontend/editor-ui/src/composables/useDataSchema.ts +++ b/packages/frontend/editor-ui/src/composables/useDataSchema.ts @@ -245,6 +245,7 @@ export type RenderItem = { id: string; icon: string; collapsable?: boolean; + nodeName?: string; nodeType?: INodeUi['type']; preview?: boolean; type: 'item'; @@ -371,6 +372,7 @@ export const useFlattenSchema = () => { icon: getIconBySchemaType(schema.type), id, collapsable: true, + nodeName: node.name, nodeType: node.type, type: 'item', preview, @@ -409,6 +411,7 @@ export const useFlattenSchema = () => { icon: getIconBySchemaType(schema.type), collapsable: false, nodeType: node.type, + nodeName: node.name, type: 'item', preview, },