diff --git a/web/app/components/workflow-app/hooks/__tests__/use-workflow-run.spec.ts b/web/app/components/workflow-app/hooks/__tests__/use-workflow-run.spec.ts index c42fb0b03f9..d635e2bc3f8 100644 --- a/web/app/components/workflow-app/hooks/__tests__/use-workflow-run.spec.ts +++ b/web/app/components/workflow-app/hooks/__tests__/use-workflow-run.spec.ts @@ -29,6 +29,17 @@ type WorkflowStoreState = { setListeningTriggerNodeId?: (value: string | null) => void } +type WorkflowRunFailedTrackingEvent = { + workflow_id: string + reason?: string + node_type?: string + data: { + workflow_status?: string + workflow_tracing_count?: number + workflow_data_chunks?: string[] + } +} + const mocks = vi.hoisted(() => { const appStoreState = { appDetail: { @@ -445,8 +456,7 @@ describe('useWorkflowRun', () => { data: { workflow_status: WorkflowRunningStatus.Running, workflow_tracing_count: 1, - workflow_data: workflowData, - workflow_data_json: JSON.stringify(workflowData), + workflow_data_chunks: [JSON.stringify(workflowData)], }, }) @@ -460,12 +470,54 @@ describe('useWorkflowRun', () => { data: { workflow_status: WorkflowRunningStatus.Running, workflow_tracing_count: 1, - workflow_data: workflowData, - workflow_data_json: JSON.stringify(workflowData), + workflow_data_chunks: [JSON.stringify(workflowData)], }, }) }) + it('should split serialized workflow data into chunks when it exceeds the amplitude string limit', async () => { + const { result } = renderHook(() => useWorkflowRun()) + + await act(async () => { + await result.current.handleRun({ inputs: { query: 'hello' } }) + }) + + const baseCallbackFactoryContext = mocks.mockCreateBaseWorkflowRunCallbacks.mock.calls.at(-1)?.[0] as { + trackWorkflowRunFailed: (params: unknown, workflowData: unknown) => void + } + const workflowData = { + result: { + status: WorkflowRunningStatus.Running, + outputs: { + text: 'x'.repeat(2200), + }, + }, + tracing: [{ node_id: 'node-1', status: 'running' }], + } + + baseCallbackFactoryContext.trackWorkflowRunFailed({ error: 'failed', node_type: 'llm' }, workflowData) + + const trackingEvent = mocks.mockTrackEvent.mock.calls.at(-1)?.[1] as WorkflowRunFailedTrackingEvent + const chunks = trackingEvent.data.workflow_data_chunks + if (!chunks) + throw new Error('Expected workflow data chunks to be tracked') + + expect(mocks.mockTrackEvent).toHaveBeenCalledWith('workflow_run_failed', { + workflow_id: 'flow-1', + reason: 'failed', + node_type: 'llm', + data: { + workflow_status: WorkflowRunningStatus.Running, + workflow_tracing_count: 1, + workflow_data_chunks: chunks, + }, + }) + expect(chunks.length).toBeGreaterThan(1) + expect(chunks.every(chunk => chunk.length <= 900)).toBe(true) + expect(chunks.join('')).toBe(JSON.stringify(workflowData)) + expect(trackingEvent.data).not.toHaveProperty('workflow_data') + }) + it('should track workflow failures when the error or workflow data is malformed', async () => { const { result } = renderHook(() => useWorkflowRun()) @@ -486,8 +538,6 @@ describe('useWorkflowRun', () => { data: { workflow_status: undefined, workflow_tracing_count: undefined, - workflow_data: undefined, - workflow_data_json: undefined, }, }) @@ -507,8 +557,6 @@ describe('useWorkflowRun', () => { data: { workflow_status: undefined, workflow_tracing_count: undefined, - workflow_data: circularWorkflowData, - workflow_data_json: undefined, }, }) }) diff --git a/web/app/components/workflow-app/hooks/use-workflow-run.ts b/web/app/components/workflow-app/hooks/use-workflow-run.ts index 7254c646ee9..fc8b3060f88 100644 --- a/web/app/components/workflow-app/hooks/use-workflow-run.ts +++ b/web/app/components/workflow-app/hooks/use-workflow-run.ts @@ -67,6 +67,8 @@ type WorkflowDebugWindow = Window & { __allTriggersDebugAbortController?: DebugAbortController } +const WORKFLOW_DATA_CHUNK_SIZE = 900 + const stringifyWorkflowData = (workflowData: unknown) => { if (!workflowData) return undefined @@ -79,6 +81,26 @@ const stringifyWorkflowData = (workflowData: unknown) => { } } +const chunkWorkflowData = (serializedWorkflowData: string) => { + const workflowDataChars = Array.from(serializedWorkflowData) + const chunks: string[] = [] + + for (let index = 0; index < workflowDataChars.length; index += WORKFLOW_DATA_CHUNK_SIZE) + chunks.push(workflowDataChars.slice(index, index + WORKFLOW_DATA_CHUNK_SIZE).join('')) + + return chunks +} + +const buildWorkflowDataTrackingProperties = (workflowData: unknown) => { + const serializedWorkflowData = stringifyWorkflowData(workflowData) + if (!serializedWorkflowData) + return {} + + return { + workflow_data_chunks: chunkWorkflowData(serializedWorkflowData), + } +} + const getWorkflowStatus = (workflowData: unknown) => { if (typeof workflowData !== 'object' || workflowData === null) return undefined @@ -367,6 +389,7 @@ const useWorkflowRunBase = (doSyncWorkflowDraft: DoSyncWorkflowDraft) => { const nodeType = typeof payload?.node_type === 'string' ? payload.node_type : undefined + const workflowDataTrackingProperties = buildWorkflowDataTrackingProperties(workflowData) trackEvent('workflow_run_failed', { workflow_id: flowId, @@ -375,8 +398,7 @@ const useWorkflowRunBase = (doSyncWorkflowDraft: DoSyncWorkflowDraft) => { data: { workflow_status: getWorkflowStatus(workflowData), workflow_tracing_count: getWorkflowTracingCount(workflowData), - workflow_data: workflowData, - workflow_data_json: stringifyWorkflowData(workflowData), + ...workflowDataTrackingProperties, }, }) }