diff --git a/cypress/e2e/6-code-node.cy.ts b/cypress/e2e/6-code-node.cy.ts index 0ed9ba5089..bd266562f5 100644 --- a/cypress/e2e/6-code-node.cy.ts +++ b/cypress/e2e/6-code-node.cy.ts @@ -110,23 +110,17 @@ describe('Code node', () => { statusCode: 200, body: { data: { - code: 'console.log("Hello World")', - usage: { - prompt_tokens: 15, - completion_tokens: 15, - total_tokens: 30 - } + code: 'console.log("Hello World")' }, } }).as('ask-ai'); + cy.getByTestId('ask-ai-cta').click(); - cy.wait('@ask-ai') - .its('request.body') - .should('deep.include', { - question: prompt, - model: "gpt-3.5-turbo-16k", - context: { schema: [] } - }); + const askAiReq = cy.wait('@ask-ai') + + askAiReq.its('request.body').should('have.keys', ['question', 'model', 'context', 'n8nVersion']); + + askAiReq.its('context').should('have.keys', ['schema', 'ndvSessionId', 'sessionId']); cy.contains('Code generation completed').should('be.visible') cy.getByTestId('code-node-tab-code').should('contain.text', 'console.log("Hello World")'); diff --git a/packages/editor-ui/src/api/ai.ts b/packages/editor-ui/src/api/ai.ts index 95ace358fb..294d3523ac 100644 --- a/packages/editor-ui/src/api/ai.ts +++ b/packages/editor-ui/src/api/ai.ts @@ -2,12 +2,6 @@ import type { IRestApiContext, Schema } from '@/Interface'; import { makeRestApiRequest } from '@/utils/apiUtils'; import type { IDataObject } from 'n8n-workflow'; -type Usage = { - prompt_tokens: number; - completion_tokens: number; - total_tokens: number; -}; - export async function generateCodeForPrompt( ctx: IRestApiContext, { @@ -20,11 +14,13 @@ export async function generateCodeForPrompt( context: { schema: Array<{ nodeName: string; schema: Schema }>; inputSchema: { nodeName: string; schema: Schema }; + sessionId: string; + ndvSessionId: string; }; model: string; n8nVersion: string; }, -): Promise<{ code: string; usage: Usage }> { +): Promise<{ code: string }> { return makeRestApiRequest(ctx, 'POST', '/ask-ai', { question, context, diff --git a/packages/editor-ui/src/components/CodeNodeEditor/AskAI/AskAI.vue b/packages/editor-ui/src/components/CodeNodeEditor/AskAI/AskAI.vue index 78787734d2..d2ac3e1410 100644 --- a/packages/editor-ui/src/components/CodeNodeEditor/AskAI/AskAI.vue +++ b/packages/editor-ui/src/components/CodeNodeEditor/AskAI/AskAI.vue @@ -9,7 +9,7 @@ import type { CodeExecutionMode, INodeExecutionData } from 'n8n-workflow'; import type { BaseTextKey } from '@/plugins/i18n'; import type { INodeUi, Schema } from '@/Interface'; import { generateCodeForPrompt } from '@/api/ai'; -import { useDataSchema, useI18n, useMessage, useToast, useTelemetry } from '@/composables'; +import { useDataSchema, useI18n, useMessage, useTelemetry, useToast } from '@/composables'; import { useNDVStore, usePostHog, useRootStore, useWorkflowsStore } from '@/stores'; import { executionDataToJson } from '@/utils'; import { @@ -131,10 +131,6 @@ async function onSubmit() { if (!activeNode) return; const schemas = getSchemas(); - useTelemetry().trackAskAI('ask.generationClicked', { - prompt: prompt.value, - }); - if (props.hasChanges) { const confirmModal = await alert(i18n.baseText('codeNodeEditor.askAi.areYouSureToReplace'), { title: i18n.baseText('codeNodeEditor.askAi.replaceCurrentCode'), @@ -157,9 +153,14 @@ async function onSubmit() { ? 'gpt-4' : 'gpt-3.5-turbo-16k'; - const { code, usage } = await generateCodeForPrompt(getRestApiContext, { + const { code } = await generateCodeForPrompt(getRestApiContext, { question: prompt.value, - context: { schema: schemas.parentNodesSchemas, inputSchema: schemas.inputSchema! }, + context: { + schema: schemas.parentNodesSchemas, + inputSchema: schemas.inputSchema!, + ndvSessionId: useNDVStore().sessionId, + sessionId: useRootStore().sessionId, + }, model, n8nVersion: version, }); @@ -170,6 +171,10 @@ async function onSubmit() { type: 'success', title: i18n.baseText('codeNodeEditor.askAi.generationCompleted'), }); + useTelemetry().trackAskAI('askAi.generationFinished', { + prompt: prompt.value, + code, + }); } catch (error) { showMessage({ type: 'error', @@ -177,6 +182,11 @@ async function onSubmit() { message: getErrorMessageByStatusCode(error.httpStatusCode || error?.response.status), }); stopLoading(); + useTelemetry().trackAskAI('askAi.generationFinished', { + prompt: prompt.value, + code: '', + hasError: true, + }); } } function triggerLoadingChange() { diff --git a/packages/editor-ui/src/components/NodeExecuteButton.vue b/packages/editor-ui/src/components/NodeExecuteButton.vue index ca1ae434b8..e7c936c72a 100644 --- a/packages/editor-ui/src/components/NodeExecuteButton.vue +++ b/packages/editor-ui/src/components/NodeExecuteButton.vue @@ -99,10 +99,10 @@ export default defineComponent({ return Boolean(this.nodeType && this.nodeType.name === MANUAL_TRIGGER_NODE_TYPE); }, isPollingTypeNode(): boolean { - return !!(this.nodeType && this.nodeType.polling); + return !!this.nodeType?.polling; }, isScheduleTrigger(): boolean { - return !!(this.nodeType && this.nodeType.group.includes('schedule')); + return !!this.nodeType?.group.includes('schedule'); }, isWebhookNode(): boolean { return Boolean(this.nodeType && this.nodeType.name === WEBHOOK_NODE_TYPE); @@ -129,9 +129,7 @@ export default defineComponent({ }, hasIssues(): boolean { return Boolean( - this.node && - this.node.issues && - (this.node.issues.parameters || this.node.issues.credentials), + this.node?.issues && (this.node.issues.parameters || this.node.issues.credentials), ); }, disabledHint(): string { @@ -171,7 +169,7 @@ export default defineComponent({ return this.$locale.baseText('ndv.execute.listenForTestEvent'); } - if (this.isPollingTypeNode || (this.nodeType && this.nodeType.mockManualExecution)) { + if (this.isPollingTypeNode || this.nodeType?.mockManualExecution) { return this.$locale.baseText('ndv.execute.fetchEvent'); } @@ -221,6 +219,7 @@ export default defineComponent({ node_type: this.nodeType ? this.nodeType.name : null, workflow_id: this.workflowsStore.workflowId, source: this.telemetrySource, + session_id: this.ndvStore.sessionId, }; this.$telemetry.track('User clicked execute node button', telemetryPayload); await this.$externalHooks().run('nodeExecuteButton.onClick', telemetryPayload); diff --git a/packages/editor-ui/src/plugins/telemetry/index.ts b/packages/editor-ui/src/plugins/telemetry/index.ts index 8ee81f4252..69864d3ca0 100644 --- a/packages/editor-ui/src/plugins/telemetry/index.ts +++ b/packages/editor-ui/src/plugins/telemetry/index.ts @@ -9,6 +9,7 @@ import { useRootStore } from '@/stores/n8nRoot.store'; import { useTelemetryStore } from '@/stores/telemetry.store'; import { SLACK_NODE_TYPE } from '@/constants'; import { usePostHog } from '@/stores/posthog.store'; +import { useNDVStore } from '@/stores'; export class Telemetry { private pageEventQueue: Array<{ route: RouteLocation }>; @@ -134,9 +135,11 @@ export class Telemetry { trackAskAI(event: string, properties: IDataObject = {}) { if (this.rudderStack) { properties.session_id = useRootStore().sessionId; + properties.ndv_session_id = useNDVStore().sessionId; + switch (event) { - case 'ask.generationClicked': - this.track('User clicked on generate code button', properties, { withPostHog: true }); + case 'askAi.generationFinished': + this.track('Ai code generation finished', properties, { withPostHog: true }); default: break; } @@ -222,7 +225,14 @@ export class Telemetry { switch (nodeType) { case SLACK_NODE_TYPE: if (change.name === 'parameters.otherOptions.includeLinkToWorkflow') { - this.track('User toggled n8n reference option'); + this.track( + 'User toggled n8n reference option', + { + node: nodeType, + toValue: change.value, + }, + { withPostHog: true }, + ); } break; diff --git a/packages/editor-ui/src/stores/ndv.store.ts b/packages/editor-ui/src/stores/ndv.store.ts index 7b884219fd..d27b4afb29 100644 --- a/packages/editor-ui/src/stores/ndv.store.ts +++ b/packages/editor-ui/src/stores/ndv.store.ts @@ -9,6 +9,7 @@ import type { } from '@/Interface'; import type { INodeIssues, IRunData } from 'n8n-workflow'; import { defineStore } from 'pinia'; +import { v4 as uuid } from 'uuid'; import { useWorkflowsStore } from './workflows.store'; export const useNDVStore = defineStore(STORES.NDV, { @@ -163,7 +164,7 @@ export const useNDVStore = defineStore(STORES.NDV, { }; }, setNDVSessionId(): void { - this.sessionId = `ndv-${Math.random().toString(36).slice(-8)}`; + this.sessionId = `ndv-${uuid()}`; }, resetNDVSessionId(): void { this.sessionId = ''; diff --git a/packages/editor-ui/src/views/NodeView.vue b/packages/editor-ui/src/views/NodeView.vue index d286ceea86..14a2139df1 100644 --- a/packages/editor-ui/src/views/NodeView.vue +++ b/packages/editor-ui/src/views/NodeView.vue @@ -680,6 +680,7 @@ export default defineComponent({ node_type: node ? node.type : null, workflow_id: this.workflowsStore.workflowId, source: 'canvas', + session_id: this.ndvStore.sessionId, }; this.$telemetry.track('User clicked execute node button', telemetryPayload); void this.$externalHooks().run('nodeView.onRunNode', telemetryPayload);