diff --git a/apps/sim/app/workspace/[workspaceId]/logs/components/frozen-canvas/frozen-canvas.tsx b/apps/sim/app/workspace/[workspaceId]/logs/components/frozen-canvas/frozen-canvas.tsx index 6e892100b2..0036bf7420 100644 --- a/apps/sim/app/workspace/[workspaceId]/logs/components/frozen-canvas/frozen-canvas.tsx +++ b/apps/sim/app/workspace/[workspaceId]/logs/components/frozen-canvas/frozen-canvas.tsx @@ -148,16 +148,81 @@ function getCurrentIterationData(blockExecutionData: any) { } } -function PinnedLogs({ executionData, onClose }: { executionData: any; onClose: () => void }) { +function PinnedLogs({ + executionData, + blockId, + workflowState, + onClose +}: { + executionData: any | null; + blockId: string; + workflowState: any; + onClose: () => void; +}) { + // ALL HOOKS MUST BE CALLED BEFORE ANY CONDITIONAL RETURNS const [currentIterationIndex, setCurrentIterationIndex] = useState(0) + // Reset iteration index when execution data changes + useEffect(() => { + setCurrentIterationIndex(0) + }, [executionData]) + + // Handle case where block has no execution data (e.g., failed workflow) + if (!executionData) { + const blockInfo = workflowState?.blocks?.[blockId] + const formatted = { + blockName: blockInfo?.name || 'Unknown Block', + blockType: blockInfo?.type || 'unknown', + status: 'not_executed', + duration: 'N/A', + input: null, + output: null, + errorMessage: null, + errorStackTrace: null, + cost: null, + tokens: null, + } + + return ( + + +
+ + + {formatted.blockName} + + +
+
+
+ + {formatted.blockType} + + not executed +
+
+
+ + +
+
+ This block was not executed because the workflow failed before reaching it. +
+
+
+
+ ) + } + + // Now we can safely use the execution data const iterationInfo = getCurrentIterationData({ ...executionData, currentIteration: currentIterationIndex, }) const formatted = formatExecutionData(iterationInfo.executionData) - const totalIterations = executionData.iterations?.length || 1 const goToPreviousIteration = () => { @@ -172,10 +237,6 @@ function PinnedLogs({ executionData, onClose }: { executionData: any; onClose: ( } } - useEffect(() => { - setCurrentIterationIndex(0) - }, [executionData]) - return ( @@ -337,17 +398,42 @@ export function FrozenCanvas({ if (traceSpans && Array.isArray(traceSpans)) { const blockExecutionMap: Record = {} - const workflowSpan = traceSpans[0] - if (workflowSpan?.children && Array.isArray(workflowSpan.children)) { - const traceSpansByBlockId = workflowSpan.children.reduce((acc: any, span: any) => { + logger.debug('Processing trace spans for frozen canvas:', { traceSpans }) + + // Recursively collect all spans with blockId from the trace spans tree + const collectBlockSpans = (spans: any[]): any[] => { + const blockSpans: any[] = [] + + for (const span of spans) { + // If this span has a blockId, it's a block execution if (span.blockId) { - if (!acc[span.blockId]) { - acc[span.blockId] = [] - } - acc[span.blockId].push(span) + blockSpans.push(span) } - return acc - }, {}) + + // Recursively check children + if (span.children && Array.isArray(span.children)) { + blockSpans.push(...collectBlockSpans(span.children)) + } + } + + return blockSpans + } + + const allBlockSpans = collectBlockSpans(traceSpans) + logger.debug('Collected all block spans:', allBlockSpans) + + // Group spans by blockId + const traceSpansByBlockId = allBlockSpans.reduce((acc: any, span: any) => { + if (span.blockId) { + if (!acc[span.blockId]) { + acc[span.blockId] = [] + } + acc[span.blockId].push(span) + } + return acc + }, {}) + + logger.debug('Grouped trace spans by blockId:', traceSpansByBlockId) for (const [blockId, spans] of Object.entries(traceSpansByBlockId)) { const spanArray = spans as any[] @@ -407,10 +493,9 @@ export function FrozenCanvas({ totalIterations: iterations.length, } } - } - setBlockExecutions(blockExecutionMap) - } + setBlockExecutions(blockExecutionMap) + } }, [traceSpans]) useEffect(() => { @@ -439,8 +524,6 @@ export function FrozenCanvas({ fetchData() }, [executionId]) - // No need to create a temporary workflow - just use the workflowState directly - if (loading) { return (
@@ -502,16 +585,18 @@ export function FrozenCanvas({ showSubBlocks={true} isPannable={true} onNodeClick={(blockId) => { - if (blockExecutions[blockId]) { - setPinnedBlockId(blockId) - } + // Always allow clicking blocks, even if they don't have execution data + // This is important for failed workflows where some blocks never executed + setPinnedBlockId(blockId) }} />
- {pinnedBlockId && blockExecutions[pinnedBlockId] && ( + {pinnedBlockId && ( setPinnedBlockId(null)} /> )} diff --git a/apps/sim/lib/logs/enhanced-execution-logger.ts b/apps/sim/lib/logs/enhanced-execution-logger.ts index e32348b4a0..3917bfd4d7 100644 --- a/apps/sim/lib/logs/enhanced-execution-logger.ts +++ b/apps/sim/lib/logs/enhanced-execution-logger.ts @@ -218,8 +218,20 @@ export class EnhancedExecutionLogger implements IExecutionLoggerService { logger.debug(`Completing workflow execution ${executionId}`) - const level = 'info' - const message = `Workflow execution completed` + // Determine if workflow failed by checking trace spans for errors + const hasErrors = traceSpans && traceSpans.some((span: any) => { + const checkSpanForErrors = (s: any): boolean => { + if (s.status === 'error') return true + if (s.children && Array.isArray(s.children)) { + return s.children.some(checkSpanForErrors) + } + return false + } + return checkSpanForErrors(span) + }) + + const level = hasErrors ? 'error' : 'info' + const message = hasErrors ? 'Workflow execution failed' : 'Workflow execution completed' const [updatedLog] = await db .update(workflowExecutionLogs)