From 98b5e2a0da707de9015267625d6584523237cab7 Mon Sep 17 00:00:00 2001 From: Xin Wang Date: Sat, 18 Apr 2026 23:23:54 +0800 Subject: [PATCH] Add trace Excel sheet, full JSON export, and cell chunking for large I/O Made-with: Cursor --- src/App.tsx | 148 +++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 147 insertions(+), 1 deletion(-) diff --git a/src/App.tsx b/src/App.tsx index 06d264a..f77c5df 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -719,9 +719,120 @@ export default function App() { const workbook = XLSX.utils.book_new(); XLSX.utils.book_append_sheet(workbook, worksheet, 'Results'); + + // Second sheet: per-node execution detail for analysis. + // Only created when at least one iteration has trace data. + if (includeTrace) { + const traceHeader = [ + 'Case', 'Iter', 'Step', + 'Node Type', 'Node Title', 'Node ID', + 'Status', 'Elapsed (s)', 'Tokens', 'Error', + 'Inputs', 'Outputs', + 'Workflow Run ID', 'Cont.' + ]; + const traceRows: Record[] = []; + + testCases.forEach((c, i) => { + c.results.forEach((r, j) => { + if (!r.trace?.length) return; + r.trace.forEach(step => { + const inputsStr = step.inputs === undefined ? '' : safeStringifyForCell(step.inputs); + const outputsStr = step.outputs === undefined ? '' : safeStringifyForCell(step.outputs); + + // A single node's input or output can exceed Excel's 32,767 char + // cell cap; chunk across sibling rows and annotate via "Cont.". + const inChunks = chunkForExcel(inputsStr); + const outChunks = chunkForExcel(outputsStr); + const chunkCount = Math.max(1, inChunks.length, outChunks.length); + + for (let k = 0; k < chunkCount; k++) { + traceRows.push({ + 'Case': i + 1, + 'Iter': j + 1, + 'Step': step.index ?? '', + 'Node Type': step.nodeType, + 'Node Title': step.title, + 'Node ID': step.nodeId, + 'Status': step.status, + 'Elapsed (s)': k === 0 && step.elapsedTime != null + ? Number(step.elapsedTime.toFixed(3)) + : '', + 'Tokens': k === 0 ? (step.tokens ?? '') : '', + 'Error': k === 0 ? (step.error ?? '') : '', + 'Inputs': inChunks[k] ?? '', + 'Outputs': outChunks[k] ?? '', + 'Workflow Run ID': k === 0 ? (r.workflowRunId ?? '') : '', + 'Cont.': chunkCount > 1 ? `${k + 1}/${chunkCount}` : '' + }); + } + }); + }); + }); + + const traceSheet = XLSX.utils.json_to_sheet(traceRows, { header: traceHeader }); + traceSheet['!cols'] = traceHeader.map(h => { + if (h === 'Inputs' || h === 'Outputs') return { wch: 60 }; + if (h === 'Node Title' || h === 'Error') return { wch: 30 }; + if (h === 'Workflow Run ID') return { wch: 38 }; + if (h === 'Node ID') return { wch: 20 }; + return { wch: 10 }; + }); + // Freeze the header row so the columns stay visible while scrolling. + traceSheet['!freeze'] = { xSplit: 0, ySplit: 1 } as any; + + XLSX.utils.book_append_sheet(workbook, traceSheet, 'TraceSteps'); + } + XLSX.writeFile(workbook, `dify_batch_test_${new Date().toISOString().split('T')[0]}.xlsx`); }; + // Step 3: Export full, lossless trace bundle as JSON for deep analysis. + const exportTraceJson = () => { + const bundle = { + exportedAt: new Date().toISOString(), + appMode: difyConfig.appMode, + apiUrl: difyConfig.apiUrl, + totals: { + cases: testCases.length, + iterations: testCases.reduce((a, c) => a + c.results.length, 0), + successes: testCases.reduce((a, c) => a + c.results.filter(r => !r.error).length, 0), + failures: testCases.reduce((a, c) => a + c.results.filter(r => !!r.error).length, 0) + }, + cases: testCases.map((c, i) => ({ + caseIndex: i + 1, + id: c.id, + inputs: c.inputs, + query: c.query, + times: c.times, + results: c.results.map((r, j) => ({ + iteration: j + 1, + workflowRunId: r.workflowRunId, + taskId: r.task_id, + messageId: r.id, + finalStatus: r.finalStatus, + totalTokens: r.totalTokens, + elapsedTime: r.elapsedTime, + error: r.error, + answer: r.answer, + data: r.data, + trace: r.trace + })) + })) + }; + + const json = JSON.stringify(bundle, null, 2); + const blob = new Blob([json], { type: 'application/json' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = `dify_batch_trace_${new Date().toISOString().split('T')[0]}.json`; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); + toast.success('追踪数据已导出为 JSON'); + }; + const getFlatResults = () => { return testCases.flatMap((testCase, index) => { return testCase.results.map((result, rIndex) => { @@ -1347,7 +1458,19 @@ export default function App() {

结果透视

-
+
+ {anyTrace && ( + + )}