From 13a91113fde1f2a4028d371604d4ea6a876ffce9 Mon Sep 17 00:00:00 2001 From: Waleed Date: Tue, 10 Feb 2026 21:21:45 -0800 Subject: [PATCH] fix(logs): surface handled errors as info in logs (#3190) * fix(logs): surface handled errors as info in logs * pr comments --- .../components/trace-spans/trace-spans.tsx | 26 +- .../workflow-edge/workflow-edge.tsx | 1 - .../hooks/use-workflow-execution.ts | 5 +- apps/sim/executor/execution/block-executor.ts | 3 + apps/sim/executor/types.ts | 2 + apps/sim/lib/logs/execution/logger.ts | 5 +- .../sim/lib/logs/execution/logging-session.ts | 2 +- .../execution/trace-spans/trace-spans.test.ts | 529 ++++++++++++++++++ .../logs/execution/trace-spans/trace-spans.ts | 31 +- apps/sim/lib/logs/types.ts | 2 + apps/sim/stores/logs/filters/types.ts | 1 + 11 files changed, 586 insertions(+), 21 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/components/trace-spans/trace-spans.tsx b/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/components/trace-spans/trace-spans.tsx index dd822327e..df55d47f8 100644 --- a/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/components/trace-spans/trace-spans.tsx +++ b/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/components/trace-spans/trace-spans.tsx @@ -67,7 +67,7 @@ function parseTime(value?: string | number | null): number { } /** - * Checks if a span or any of its descendants has an error + * Checks if a span or any of its descendants has an error (any error). */ function hasErrorInTree(span: TraceSpan): boolean { if (span.status === 'error') return true @@ -80,6 +80,23 @@ function hasErrorInTree(span: TraceSpan): boolean { return false } +/** + * Checks if a span or any of its descendants has an unhandled error. + * Spans with errorHandled: true (including containers that propagate it) + * are skipped. Used only for the root workflow span to match the actual + * workflow status. + */ +function hasUnhandledErrorInTree(span: TraceSpan): boolean { + if (span.status === 'error' && !span.errorHandled) return true + if (span.children && span.children.length > 0) { + return span.children.some((child) => hasUnhandledErrorInTree(child)) + } + if (span.toolCalls && span.toolCalls.length > 0 && !span.errorHandled) { + return span.toolCalls.some((tc) => tc.error) + } + return false +} + /** * Normalizes and sorts trace spans recursively. * Deduplicates children and sorts by start time. @@ -478,14 +495,13 @@ const TraceSpanNode = memo(function TraceSpanNode({ const duration = span.duration || spanEndTime - spanStartTime const isDirectError = span.status === 'error' - const hasNestedError = hasErrorInTree(span) + const isRootWorkflow = depth === 0 + const isRootWorkflowSpan = isRootWorkflow && span.type?.toLowerCase() === 'workflow' + const hasNestedError = isRootWorkflowSpan ? hasUnhandledErrorInTree(span) : hasErrorInTree(span) const showErrorStyle = isDirectError || hasNestedError const { icon: BlockIcon, bgColor } = getBlockIconAndColor(span.type, span.name) - // Root workflow execution is always expanded and has no toggle - const isRootWorkflow = depth === 0 - // Build all children including tool calls const allChildren = useMemo(() => { const children: TraceSpan[] = [] diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-edge/workflow-edge.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-edge/workflow-edge.tsx index 3a7a433a6..3b0b85ed7 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-edge/workflow-edge.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-edge/workflow-edge.tsx @@ -95,7 +95,6 @@ const WorkflowEdgeComponent = ({ color = 'var(--brand-tertiary-2)' } else if (edgeRunStatus === 'success') { // Use green for preview mode, default for canvas execution - // This also applies to error edges that were taken (error path executed) color = previewExecutionStatus ? 'var(--brand-tertiary-2)' : 'var(--border-success)' } else if (edgeRunStatus === 'error') { color = 'var(--text-error)' diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-workflow-execution.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-workflow-execution.ts index 6a2a43f67..16c0e81f1 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-workflow-execution.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-workflow-execution.ts @@ -55,7 +55,7 @@ interface DebugValidationResult { interface BlockEventHandlerConfig { workflowId?: string executionId?: string - workflowEdges: Array<{ id: string; target: string }> + workflowEdges: Array<{ id: string; target: string; sourceHandle?: string | null }> activeBlocksSet: Set accumulatedBlockLogs: BlockLog[] accumulatedBlockStates: Map @@ -322,7 +322,8 @@ export function useWorkflowExecution() { if (!workflowId) return const incomingEdges = workflowEdges.filter((edge) => edge.target === blockId) incomingEdges.forEach((edge) => { - setEdgeRunStatus(workflowId, edge.id, 'success') + const status = edge.sourceHandle === 'error' ? 'error' : 'success' + setEdgeRunStatus(workflowId, edge.id, status) }) } diff --git a/apps/sim/executor/execution/block-executor.ts b/apps/sim/executor/execution/block-executor.ts index fcd5c1297..58143e583 100644 --- a/apps/sim/executor/execution/block-executor.ts +++ b/apps/sim/executor/execution/block-executor.ts @@ -292,6 +292,9 @@ export class BlockExecutor { const hasErrorPort = this.hasErrorPortEdge(node) if (hasErrorPort) { + if (blockLog) { + blockLog.errorHandled = true + } logger.info('Block has error port - returning error output instead of throwing', { blockId: node.id, error: errorMessage, diff --git a/apps/sim/executor/types.ts b/apps/sim/executor/types.ts index b8bcb70f1..9298f6667 100644 --- a/apps/sim/executor/types.ts +++ b/apps/sim/executor/types.ts @@ -112,6 +112,8 @@ export interface BlockLog { output?: any input?: any error?: string + /** Whether this error was handled by an error handler path (error port) */ + errorHandled?: boolean loopId?: string parallelId?: string iterationIndex?: number diff --git a/apps/sim/lib/logs/execution/logger.ts b/apps/sim/lib/logs/execution/logger.ts index cde4df3c6..2bc62d4eb 100644 --- a/apps/sim/lib/logs/execution/logger.ts +++ b/apps/sim/lib/logs/execution/logger.ts @@ -219,11 +219,12 @@ export class ExecutionLogger implements IExecutionLoggerService { | { traceSpans?: TraceSpan[] } | undefined - // Determine if workflow failed by checking trace spans for errors + // Determine if workflow failed by checking trace spans for unhandled errors + // Errors handled by error handler paths (errorHandled: true) don't count as workflow failures // Use the override if provided (for cost-only fallback scenarios) const hasErrors = traceSpans?.some((span: any) => { const checkSpanForErrors = (s: any): boolean => { - if (s.status === 'error') return true + if (s.status === 'error' && !s.errorHandled) return true if (s.children && Array.isArray(s.children)) { return s.children.some(checkSpanForErrors) } diff --git a/apps/sim/lib/logs/execution/logging-session.ts b/apps/sim/lib/logs/execution/logging-session.ts index 9ab710dc1..c0e943ee8 100644 --- a/apps/sim/lib/logs/execution/logging-session.ts +++ b/apps/sim/lib/logs/execution/logging-session.ts @@ -301,7 +301,7 @@ export class LoggingSession { const hasErrors = traceSpans.some((span: any) => { const checkForErrors = (s: any): boolean => { - if (s.status === 'error') return true + if (s.status === 'error' && !s.errorHandled) return true if (s.children && Array.isArray(s.children)) { return s.children.some(checkForErrors) } diff --git a/apps/sim/lib/logs/execution/trace-spans/trace-spans.test.ts b/apps/sim/lib/logs/execution/trace-spans/trace-spans.test.ts index 987318d5e..bb7d5250a 100644 --- a/apps/sim/lib/logs/execution/trace-spans/trace-spans.test.ts +++ b/apps/sim/lib/logs/execution/trace-spans/trace-spans.test.ts @@ -990,6 +990,535 @@ describe('buildTraceSpans', () => { }) }) +describe('errorHandled - handled errors should not bubble up', () => { + it.concurrent('block span stays error but is marked errorHandled', () => { + const result: ExecutionResult = { + success: true, + output: { content: 'done' }, + logs: [ + { + blockId: 'api-1', + blockName: 'API Call', + blockType: 'api', + startedAt: '2024-01-01T10:00:00.000Z', + endedAt: '2024-01-01T10:00:01.000Z', + durationMs: 1000, + success: false, + error: 'Request failed with status 500', + errorHandled: true, + executionOrder: 1, + }, + { + blockId: 'fallback-1', + blockName: 'Fallback', + blockType: 'function', + startedAt: '2024-01-01T10:00:01.000Z', + endedAt: '2024-01-01T10:00:02.000Z', + durationMs: 1000, + success: true, + executionOrder: 2, + }, + ], + } + + const { traceSpans } = buildTraceSpans(result) + + const apiSpan = traceSpans.find((s) => s.blockId === 'api-1')! + expect(apiSpan.status).toBe('error') + expect(apiSpan.errorHandled).toBe(true) + expect((apiSpan.output as { error?: string }).error).toBe('Request failed with status 500') + + const fallbackSpan = traceSpans.find((s) => s.blockId === 'fallback-1')! + expect(fallbackSpan.status).toBe('success') + expect(fallbackSpan.errorHandled).toBeUndefined() + }) + + it.concurrent('unhandled errors still produce error status', () => { + const result: ExecutionResult = { + success: false, + output: {}, + metadata: { duration: 1000, startTime: '2024-01-01T10:00:00.000Z' }, + logs: [ + { + blockId: 'api-1', + blockName: 'API Call', + blockType: 'api', + startedAt: '2024-01-01T10:00:00.000Z', + endedAt: '2024-01-01T10:00:01.000Z', + durationMs: 1000, + success: false, + error: 'Request failed with status 500', + executionOrder: 1, + }, + ], + } + + const { traceSpans } = buildTraceSpans(result) + + const workflowSpan = traceSpans[0] + expect(workflowSpan.name).toBe('Workflow Execution') + expect(workflowSpan.status).toBe('error') + + const apiSpan = workflowSpan.children![0] + expect(apiSpan.status).toBe('error') + expect(apiSpan.errorHandled).toBeUndefined() + }) + + it.concurrent('workflow-level span is success when all errors are handled', () => { + const result: ExecutionResult = { + success: true, + output: { content: 'recovered' }, + metadata: { duration: 2000, startTime: '2024-01-01T10:00:00.000Z' }, + logs: [ + { + blockId: 'api-1', + blockName: 'API Call', + blockType: 'api', + startedAt: '2024-01-01T10:00:00.000Z', + endedAt: '2024-01-01T10:00:01.000Z', + durationMs: 1000, + success: false, + error: 'Connection timeout', + errorHandled: true, + executionOrder: 1, + }, + { + blockId: 'handler-1', + blockName: 'Error Handler', + blockType: 'function', + startedAt: '2024-01-01T10:00:01.000Z', + endedAt: '2024-01-01T10:00:02.000Z', + durationMs: 1000, + success: true, + executionOrder: 2, + }, + ], + } + + const { traceSpans } = buildTraceSpans(result) + + const workflowSpan = traceSpans[0] + expect(workflowSpan.name).toBe('Workflow Execution') + expect(workflowSpan.status).toBe('success') + }) + + it.concurrent( + 'workflow-level span is error when there is a mix of handled and unhandled errors', + () => { + const result: ExecutionResult = { + success: false, + output: {}, + metadata: { duration: 3000, startTime: '2024-01-01T10:00:00.000Z' }, + logs: [ + { + blockId: 'api-1', + blockName: 'API Call (handled)', + blockType: 'api', + startedAt: '2024-01-01T10:00:00.000Z', + endedAt: '2024-01-01T10:00:01.000Z', + durationMs: 1000, + success: false, + error: 'Handled error', + errorHandled: true, + executionOrder: 1, + }, + { + blockId: 'handler-1', + blockName: 'Error Handler', + blockType: 'function', + startedAt: '2024-01-01T10:00:01.000Z', + endedAt: '2024-01-01T10:00:02.000Z', + durationMs: 1000, + success: true, + executionOrder: 2, + }, + { + blockId: 'api-2', + blockName: 'API Call (unhandled)', + blockType: 'api', + startedAt: '2024-01-01T10:00:02.000Z', + endedAt: '2024-01-01T10:00:03.000Z', + durationMs: 1000, + success: false, + error: 'Unhandled crash', + executionOrder: 3, + }, + ], + } + + const { traceSpans } = buildTraceSpans(result) + + const workflowSpan = traceSpans[0] + expect(workflowSpan.name).toBe('Workflow Execution') + expect(workflowSpan.status).toBe('error') + + const handledSpan = workflowSpan.children!.find((s) => s.blockId === 'api-1')! + expect(handledSpan.status).toBe('error') + expect(handledSpan.errorHandled).toBe(true) + + const unhandledSpan = workflowSpan.children!.find((s) => s.blockId === 'api-2')! + expect(unhandledSpan.status).toBe('error') + expect(unhandledSpan.errorHandled).toBeUndefined() + } + ) + + it.concurrent( + 'handled errors inside loop iterations still show error on loop but not on workflow', + () => { + const result: ExecutionResult = { + success: true, + output: { content: 'all iterations recovered' }, + metadata: { duration: 5000, startTime: '2024-01-01T10:00:00.000Z' }, + logs: [ + { + blockId: 'api-1', + blockName: 'API Call (iteration 0)', + blockType: 'api', + startedAt: '2024-01-01T10:00:00.000Z', + endedAt: '2024-01-01T10:00:01.000Z', + durationMs: 1000, + success: false, + error: 'Rate limited', + errorHandled: true, + loopId: 'loop-1', + iterationIndex: 0, + executionOrder: 1, + }, + { + blockId: 'handler-1', + blockName: 'Rate Limit Handler (iteration 0)', + blockType: 'function', + startedAt: '2024-01-01T10:00:01.000Z', + endedAt: '2024-01-01T10:00:02.000Z', + durationMs: 1000, + success: true, + loopId: 'loop-1', + iterationIndex: 0, + executionOrder: 2, + }, + { + blockId: 'api-1', + blockName: 'API Call (iteration 1)', + blockType: 'api', + startedAt: '2024-01-01T10:00:02.000Z', + endedAt: '2024-01-01T10:00:03.000Z', + durationMs: 1000, + success: true, + loopId: 'loop-1', + iterationIndex: 1, + executionOrder: 3, + }, + { + blockId: 'api-1', + blockName: 'API Call (iteration 2)', + blockType: 'api', + startedAt: '2024-01-01T10:00:03.000Z', + endedAt: '2024-01-01T10:00:04.000Z', + durationMs: 1000, + success: false, + error: 'Rate limited again', + errorHandled: true, + loopId: 'loop-1', + iterationIndex: 2, + executionOrder: 4, + }, + { + blockId: 'handler-1', + blockName: 'Rate Limit Handler (iteration 2)', + blockType: 'function', + startedAt: '2024-01-01T10:00:04.000Z', + endedAt: '2024-01-01T10:00:05.000Z', + durationMs: 1000, + success: true, + loopId: 'loop-1', + iterationIndex: 2, + executionOrder: 5, + }, + ], + } + + const { traceSpans } = buildTraceSpans(result) + + const workflowSpan = traceSpans[0] + expect(workflowSpan.name).toBe('Workflow Execution') + expect(workflowSpan.status).toBe('success') + + const loopSpan = workflowSpan.children!.find((s) => s.type === 'loop')! + expect(loopSpan).toBeDefined() + expect(loopSpan.status).toBe('error') + + const iterations = loopSpan.children! + expect(iterations).toHaveLength(3) + expect(iterations[0].status).toBe('error') + expect(iterations[1].status).toBe('success') + expect(iterations[2].status).toBe('error') + } + ) + + it.concurrent( + 'handled errors inside parallel iterations still show error on parallel but not on workflow', + () => { + const result: ExecutionResult = { + success: true, + output: { content: 'parallel done' }, + metadata: { duration: 2000, startTime: '2024-01-01T10:00:00.000Z' }, + logs: [ + { + blockId: 'api-1', + blockName: 'API Call (iteration 0)', + blockType: 'api', + startedAt: '2024-01-01T10:00:00.000Z', + endedAt: '2024-01-01T10:00:01.000Z', + durationMs: 1000, + success: false, + error: 'Timeout on iteration 0', + errorHandled: true, + parallelId: 'parallel-1', + iterationIndex: 0, + executionOrder: 1, + }, + { + blockId: 'api-1', + blockName: 'API Call (iteration 1)', + blockType: 'api', + startedAt: '2024-01-01T10:00:01.000Z', + endedAt: '2024-01-01T10:00:02.000Z', + durationMs: 1000, + success: true, + parallelId: 'parallel-1', + iterationIndex: 1, + executionOrder: 2, + }, + ], + } + + const { traceSpans } = buildTraceSpans(result) + + const workflowSpan = traceSpans[0] + expect(workflowSpan.name).toBe('Workflow Execution') + expect(workflowSpan.status).toBe('success') + + const parallelSpan = workflowSpan.children!.find((s) => s.type === 'parallel')! + expect(parallelSpan).toBeDefined() + expect(parallelSpan.status).toBe('error') + + const iterations = parallelSpan.children! + expect(iterations).toHaveLength(2) + expect(iterations[0].status).toBe('error') + expect(iterations[1].status).toBe('success') + } + ) + + it.concurrent( + 'unhandled error in one loop iteration still makes the loop and workflow error', + () => { + const result: ExecutionResult = { + success: false, + output: {}, + metadata: { duration: 2000, startTime: '2024-01-01T10:00:00.000Z' }, + logs: [ + { + blockId: 'api-1', + blockName: 'API Call (iteration 0)', + blockType: 'api', + startedAt: '2024-01-01T10:00:00.000Z', + endedAt: '2024-01-01T10:00:01.000Z', + durationMs: 1000, + success: true, + loopId: 'loop-1', + iterationIndex: 0, + executionOrder: 1, + }, + { + blockId: 'api-1', + blockName: 'API Call (iteration 1)', + blockType: 'api', + startedAt: '2024-01-01T10:00:01.000Z', + endedAt: '2024-01-01T10:00:02.000Z', + durationMs: 1000, + success: false, + error: 'Unhandled crash in iteration 1', + loopId: 'loop-1', + iterationIndex: 1, + executionOrder: 2, + }, + ], + } + + const { traceSpans } = buildTraceSpans(result) + + const workflowSpan = traceSpans[0] + expect(workflowSpan.name).toBe('Workflow Execution') + expect(workflowSpan.status).toBe('error') + + const loopSpan = workflowSpan.children!.find((s) => s.type === 'loop')! + expect(loopSpan.status).toBe('error') + + const iterations = loopSpan.children! + expect(iterations[0].status).toBe('success') + expect(iterations[1].status).toBe('error') + } + ) + + it.concurrent('error output is preserved on the span even when error is handled', () => { + const result: ExecutionResult = { + success: true, + output: { content: 'recovered' }, + logs: [ + { + blockId: 'api-1', + blockName: 'Flaky API', + blockType: 'api', + startedAt: '2024-01-01T10:00:00.000Z', + endedAt: '2024-01-01T10:00:01.000Z', + durationMs: 1000, + success: false, + error: 'ECONNRESET', + errorHandled: true, + output: { error: 'ECONNRESET' }, + executionOrder: 1, + }, + ], + } + + const { traceSpans } = buildTraceSpans(result) + + const apiSpan = traceSpans[0] + expect(apiSpan.status).toBe('error') + expect(apiSpan.errorHandled).toBe(true) + expect((apiSpan.output as { error?: string }).error).toBe('ECONNRESET') + }) + + it.concurrent('block with error and errorHandled=false is treated as unhandled', () => { + const result: ExecutionResult = { + success: false, + output: {}, + metadata: { duration: 1000, startTime: '2024-01-01T10:00:00.000Z' }, + logs: [ + { + blockId: 'api-1', + blockName: 'API Call', + blockType: 'api', + startedAt: '2024-01-01T10:00:00.000Z', + endedAt: '2024-01-01T10:00:01.000Z', + durationMs: 1000, + success: false, + error: 'Server error', + errorHandled: false, + executionOrder: 1, + }, + ], + } + + const { traceSpans } = buildTraceSpans(result) + + const workflowSpan = traceSpans[0] + expect(workflowSpan.name).toBe('Workflow Execution') + expect(workflowSpan.status).toBe('error') + + const apiSpan = workflowSpan.children![0] + expect(apiSpan.status).toBe('error') + }) + + it.concurrent('many loop iterations with handled errors produce a successful workflow', () => { + const logs = [] + for (let i = 0; i < 10; i++) { + const startMs = 1704103200000 + i * 2000 + if (i % 3 === 0) { + logs.push({ + blockId: 'api-1', + blockName: `API Call (iteration ${i})`, + blockType: 'api', + startedAt: new Date(startMs).toISOString(), + endedAt: new Date(startMs + 1000).toISOString(), + durationMs: 1000, + success: false, + error: `Error in iteration ${i}`, + errorHandled: true, + loopId: 'loop-1', + iterationIndex: i, + executionOrder: i * 2 + 1, + }) + logs.push({ + blockId: 'handler-1', + blockName: `Error Handler (iteration ${i})`, + blockType: 'function', + startedAt: new Date(startMs + 1000).toISOString(), + endedAt: new Date(startMs + 2000).toISOString(), + durationMs: 1000, + success: true, + loopId: 'loop-1', + iterationIndex: i, + executionOrder: i * 2 + 2, + }) + } else { + logs.push({ + blockId: 'api-1', + blockName: `API Call (iteration ${i})`, + blockType: 'api', + startedAt: new Date(startMs).toISOString(), + endedAt: new Date(startMs + 1000).toISOString(), + durationMs: 1000, + success: true, + loopId: 'loop-1', + iterationIndex: i, + executionOrder: i * 2 + 1, + }) + } + } + + const result: ExecutionResult = { + success: true, + output: { content: 'all done' }, + metadata: { duration: 20000, startTime: '2024-01-01T10:00:00.000Z' }, + logs: logs as any, + } + + const { traceSpans } = buildTraceSpans(result) + + const workflowSpan = traceSpans[0] + expect(workflowSpan.name).toBe('Workflow Execution') + expect(workflowSpan.status).toBe('success') + + const loopSpan = workflowSpan.children!.find((s) => s.type === 'loop')! + expect(loopSpan).toBeDefined() + expect(loopSpan.status).toBe('error') + + loopSpan.children!.forEach((iteration, i) => { + if (i % 3 === 0) { + expect(iteration.status).toBe('error') + } else { + expect(iteration.status).toBe('success') + } + }) + }) + + it.concurrent('successful blocks without errors have no errorHandled flag', () => { + const result: ExecutionResult = { + success: true, + output: { content: 'fine' }, + logs: [ + { + blockId: 'text-1', + blockName: 'Text Block', + blockType: 'text', + startedAt: '2024-01-01T10:00:00.000Z', + endedAt: '2024-01-01T10:00:01.000Z', + durationMs: 1000, + success: true, + executionOrder: 1, + }, + ], + } + + const { traceSpans } = buildTraceSpans(result) + + const span = traceSpans[0] + expect(span.status).toBe('success') + expect(span.errorHandled).toBeUndefined() + }) +}) + describe('stripCustomToolPrefix', () => { it.concurrent('strips custom_ prefix from tool names', () => { expect(stripCustomToolPrefix('custom_test_tool')).toBe('test_tool') diff --git a/apps/sim/lib/logs/execution/trace-spans/trace-spans.ts b/apps/sim/lib/logs/execution/trace-spans/trace-spans.ts index a4b35330d..5fd1a92af 100644 --- a/apps/sim/lib/logs/execution/trace-spans/trace-spans.ts +++ b/apps/sim/lib/logs/execution/trace-spans/trace-spans.ts @@ -156,6 +156,7 @@ export function buildTraceSpans(result: ExecutionResult): { output: output, ...(childWorkflowSnapshotId ? { childWorkflowSnapshotId } : {}), ...(childWorkflowId ? { childWorkflowId } : {}), + ...(log.errorHandled && { errorHandled: true }), ...(log.loopId && { loopId: log.loopId }), ...(log.parallelId && { parallelId: log.parallelId }), ...(log.iterationIndex !== undefined && { iterationIndex: log.iterationIndex }), @@ -501,15 +502,11 @@ export function buildTraceSpans(result: ExecutionResult): { } addRelativeTimestamps(groupedRootSpans, earliestStart) - const hasErrors = groupedRootSpans.some((span) => { - if (span.status === 'error') return true - const checkChildren = (children: TraceSpan[] = []): boolean => { - return children.some( - (child) => child.status === 'error' || (child.children && checkChildren(child.children)) - ) - } - return span.children && checkChildren(span.children) - }) + const checkForUnhandledErrors = (s: TraceSpan): boolean => { + if (s.status === 'error' && !s.errorHandled) return true + return s.children ? s.children.some(checkForUnhandledErrors) : false + } + const hasUnhandledErrors = groupedRootSpans.some(checkForUnhandledErrors) const workflowSpan: TraceSpan = { id: 'workflow-execution', @@ -518,7 +515,7 @@ export function buildTraceSpans(result: ExecutionResult): { duration: actualWorkflowDuration, // Always use actual duration for the span startTime: new Date(earliestStart).toISOString(), endTime: new Date(latestEnd).toISOString(), - status: hasErrors ? 'error' : 'success', + status: hasUnhandledErrors ? 'error' : 'success', children: groupedRootSpans, } @@ -710,6 +707,8 @@ function groupIterationBlocks(spans: TraceSpan[]): TraceSpan[] { const iterDuration = iterLatestEnd - iterEarliestStart const hasErrors = spans.some((span) => span.status === 'error') + const allErrorsHandled = + hasErrors && spans.every((span) => span.status !== 'error' || span.errorHandled) const iterationSpan: TraceSpan = { id: `${containerId}-iteration-${iterationIndex}`, @@ -719,6 +718,7 @@ function groupIterationBlocks(spans: TraceSpan[]): TraceSpan[] { startTime: new Date(iterEarliestStart).toISOString(), endTime: new Date(iterLatestEnd).toISOString(), status: hasErrors ? 'error' : 'success', + ...(allErrorsHandled && { errorHandled: true }), children: spans.map((span) => ({ ...span, name: span.name.replace(/ \(iteration \d+\)$/, ''), @@ -729,6 +729,9 @@ function groupIterationBlocks(spans: TraceSpan[]): TraceSpan[] { }) const hasErrors = allIterationSpans.some((span) => span.status === 'error') + const allErrorsHandled = + hasErrors && + iterationChildren.every((span) => span.status !== 'error' || span.errorHandled) const parallelContainer: TraceSpan = { id: `parallel-execution-${containerId}`, name: containerName, @@ -737,6 +740,7 @@ function groupIterationBlocks(spans: TraceSpan[]): TraceSpan[] { startTime: new Date(earliestStart).toISOString(), endTime: new Date(latestEnd).toISOString(), status: hasErrors ? 'error' : 'success', + ...(allErrorsHandled && { errorHandled: true }), children: iterationChildren, } @@ -762,6 +766,8 @@ function groupIterationBlocks(spans: TraceSpan[]): TraceSpan[] { const iterDuration = iterLatestEnd - iterEarliestStart const hasErrors = spans.some((span) => span.status === 'error') + const allErrorsHandled = + hasErrors && spans.every((span) => span.status !== 'error' || span.errorHandled) const iterationSpan: TraceSpan = { id: `${containerId}-iteration-${iterationIndex}`, @@ -771,6 +777,7 @@ function groupIterationBlocks(spans: TraceSpan[]): TraceSpan[] { startTime: new Date(iterEarliestStart).toISOString(), endTime: new Date(iterLatestEnd).toISOString(), status: hasErrors ? 'error' : 'success', + ...(allErrorsHandled && { errorHandled: true }), children: spans.map((span) => ({ ...span, name: span.name.replace(/ \(iteration \d+\)$/, ''), @@ -781,6 +788,9 @@ function groupIterationBlocks(spans: TraceSpan[]): TraceSpan[] { }) const hasErrors = allIterationSpans.some((span) => span.status === 'error') + const allErrorsHandled = + hasErrors && + iterationChildren.every((span) => span.status !== 'error' || span.errorHandled) const loopContainer: TraceSpan = { id: `loop-execution-${containerId}`, name: containerName, @@ -789,6 +799,7 @@ function groupIterationBlocks(spans: TraceSpan[]): TraceSpan[] { startTime: new Date(earliestStart).toISOString(), endTime: new Date(latestEnd).toISOString(), status: hasErrors ? 'error' : 'success', + ...(allErrorsHandled && { errorHandled: true }), children: iterationChildren, } diff --git a/apps/sim/lib/logs/types.ts b/apps/sim/lib/logs/types.ts index 1b93e64e1..d2775d1ca 100644 --- a/apps/sim/lib/logs/types.ts +++ b/apps/sim/lib/logs/types.ts @@ -175,6 +175,8 @@ export interface TraceSpan { children?: TraceSpan[] toolCalls?: ToolCall[] status?: 'success' | 'error' + /** Whether this block's error was handled by an error handler path */ + errorHandled?: boolean tokens?: number | TokenInfo relativeStartMs?: number blockId?: string diff --git a/apps/sim/stores/logs/filters/types.ts b/apps/sim/stores/logs/filters/types.ts index e19f7f252..e5768d376 100644 --- a/apps/sim/stores/logs/filters/types.ts +++ b/apps/sim/stores/logs/filters/types.ts @@ -86,6 +86,7 @@ export interface TraceSpan { children?: TraceSpan[] toolCalls?: ToolCall[] status?: 'success' | 'error' + errorHandled?: boolean tokens?: number | TokenInfo relativeStartMs?: number // Time in ms from the start of the parent span blockId?: string // Added to track the original block ID for relationship mapping