fix: add web-probe finding drilldown (#733)

Co-authored-by: Codex <codex@noreply.local>
This commit is contained in:
Lyon
2026-06-23 11:14:01 +08:00
committed by GitHub
parent 9a3c71ee6d
commit ab03ead39d
2 changed files with 144 additions and 14 deletions
+73 -7
View File
@@ -82,6 +82,7 @@ interface NodeWebProbeObserveOptions {
tailLines: number;
maxFiles: number;
collectFile: string | null;
collectFinding: string | null;
analyzeArchivePrefix: string | null;
full: boolean;
stateDir: string | null;
@@ -7266,6 +7267,7 @@ function parseNodeWebProbeObserveOptions(
"--tail-lines",
"--max-files",
"--file",
"--finding",
"--archive-prefix",
"--state-dir",
"--job-id",
@@ -7284,6 +7286,8 @@ function parseNodeWebProbeObserveOptions(
if (jobId !== null && !isSafeWebObserveJobId(jobId)) throw new Error(`unsafe web-probe observe --job-id: ${jobId}`);
const collectFile = optionValue(args, "--file") ?? null;
if (collectFile !== null && !isSafeWebObserveCollectFile(collectFile)) throw new Error(`unsafe web-probe observe --file: ${collectFile}`);
const collectFinding = optionValue(args, "--finding") ?? null;
if (collectFinding !== null && !isSafeWebObserveFindingId(collectFinding)) throw new Error(`unsafe web-probe observe --finding: ${collectFinding}`);
const analyzeArchivePrefix = optionValue(args, "--archive-prefix") ?? null;
if (analyzeArchivePrefix !== null && !isSafeWebObserveArchivePrefix(analyzeArchivePrefix)) throw new Error(`unsafe web-probe observe --archive-prefix: ${analyzeArchivePrefix}`);
if (observeActionRaw !== "start" && stateDir === null && jobId === null) {
@@ -7317,6 +7321,7 @@ function parseNodeWebProbeObserveOptions(
tailLines: positiveIntegerOption(args, "--tail-lines", 5, 200),
maxFiles: positiveIntegerOption(args, "--max-files", 80, 5000),
collectFile,
collectFinding,
analyzeArchivePrefix,
full: args.includes("--full"),
stateDir,
@@ -8188,7 +8193,7 @@ function runNodeWebProbeObserveCollect(options: NodeWebProbeObserveOptions, spec
const script = [
"set -eu",
nodeWebObserveResolveStateDirShell(options),
nodeWebObserveCollectNodeScript(options.maxFiles, options.collectFile),
nodeWebObserveCollectNodeScript(options.maxFiles, options.collectFile, options.collectFinding),
].join("\n");
const result = runTransWorkspaceStdinScript(options.node, spec.workspace, script, options.commandTimeoutSeconds);
const collect = parseJsonObject(result.stdout);
@@ -8227,6 +8232,8 @@ function renderWebObserveCollectTable(value: Record<string, unknown>): string {
const jsonContentPreview = file.jsonContent === undefined || file.jsonContent === null ? "" : JSON.stringify(file.jsonContent, null, 2);
const jsonRunnerErrors = Array.isArray(jsonSummary.runnerErrors) ? jsonSummary.runnerErrors.map(record).filter((item): item is Record<string, unknown> => item !== null).slice(-8) : [];
const jsonFindings = Array.isArray(jsonSummary.findings) ? jsonSummary.findings.map(record).filter((item): item is Record<string, unknown> => item !== null).slice(0, 8) : [];
const jsonFindingDetail = record(jsonSummary.findingDetail);
const jsonFindingSamples = Array.isArray(jsonFindingDetail?.samples) ? jsonFindingDetail.samples.map(record).filter((item): item is Record<string, unknown> => item !== null).slice(0, 12) : [];
const lines = [
`hwlab nodes web-probe observe collect (${webObserveText(value.status)})`,
"",
@@ -8307,6 +8314,31 @@ function renderWebObserveCollectTable(value: Record<string, unknown>): string {
"",
);
}
if (jsonFindingDetail !== null) {
lines.push(
"Finding detail:",
webObserveTable(["ID", "SEVERITY", "COUNT", "SAMPLE_SOURCE", "SUMMARY"], [[
webObserveShort(webObserveText(jsonFindingDetail.id), 48),
webObserveText(jsonFindingDetail.severity),
webObserveText(jsonFindingDetail.count),
webObserveText(jsonFindingDetail.sampleSource),
webObserveShort(webObserveText(jsonFindingDetail.summary), 96),
]]),
"",
"Finding samples:",
webObserveTable(["SEQ", "TS", "KIND", "CONTROL", "OBSERVER", "SESSION", "PATH", "DETAIL"], jsonFindingSamples.length > 0 ? jsonFindingSamples.map((item) => [
webObserveText(item.seq ?? item.sampleSeq ?? item.fromSeq),
webObserveShort(webObserveText(item.ts ?? item.sampleTs ?? item.fromTs), 24),
webObserveShort(webObserveText(item.diffKind ?? item.kind ?? item.type ?? item.metric ?? item.anomaly), 28),
webObserveShort(webObserveText(item.control ?? item.controlValue ?? item.beforeValue), 28),
webObserveShort(webObserveText(item.observer ?? item.observerValue ?? item.afterValue), 28),
webObserveShort(webObserveText(item.sessionId ?? item.routeSessionId ?? item.activeSessionId), 36),
webObserveShort(webObserveText(item.path ?? item.urlPath ?? item.url), 48),
webObserveShort(webObserveText(item.detail ?? item.summary ?? item.message ?? item.preview), 96),
]) : [["-", "-", "-", "-", "-", "-", "-", "-"]]),
"",
);
}
if (jsonlTail.length > 0) {
lines.push(
"JSONL tail:",
@@ -9894,10 +9926,10 @@ console.log(JSON.stringify({ok:true,command:'web-probe-observe status',stateDir:
`)} "$state_dir" ${shellQuote(node)} ${shellQuote(lane)}`;
}
function nodeWebObserveCollectNodeScript(maxFiles: number, collectFile: string | null): string {
function nodeWebObserveCollectNodeScript(maxFiles: number, collectFile: string | null, collectFinding: string | null): string {
return `node -e ${shellQuote(`
const fs=require('fs'),path=require('path'),crypto=require('crypto');
const dir=process.argv[1]; const selected=process.argv[2]||''; const maxFiles=${maxFiles}; const maxReadBytes=64*1024; const maxJsonlTailBytes=1024*1024; const maxTextPreviewBytes=4096;
const dir=process.argv[1]; const selected=process.argv[2]||''; const findingFilter=process.argv[3]||''; const maxFiles=${maxFiles}; const maxReadBytes=64*1024; const maxJsonlTailBytes=1024*1024; const maxTextPreviewBytes=4096;
const shaFile=(file)=>'sha256:'+crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex');
const safeRel=(value)=>Boolean(value)&&!path.isAbsolute(value)&&!value.includes('..')&&!value.includes('\\\\')&&value.split('/').every((part)=>part&&part!=='.'&&part!=='..');
const short=(value,max=180)=>value===undefined||value===null?undefined:String(value).replace(/\\s+/g,' ').slice(0,max);
@@ -9942,6 +9974,33 @@ const slimFinding=(item)=>{
groupRows:groups
};
};
const firstArray=(...values)=>values.find((value)=>Array.isArray(value))||[];
const compactMetric=(value)=>value===undefined||value===null?undefined:(typeof value==='object'?short(JSON.stringify(slimObject(value,8)),160):short(value,160));
const compactProjectionPair=(messageCount,traceCount)=>messageCount===undefined&&traceCount===undefined?undefined:'msg='+String(messageCount??'-')+' trace='+String(traceCount??'-');
const slimFindingSample=(value)=>{
if(!value||typeof value!=='object') return null;
const controlCount=compactProjectionPair(value.controlMessageCount,value.controlTraceRowCount)??value.controlValue??value.beforeMessageCount??value.beforeValue;
const observerCount=compactProjectionPair(value.observerMessageCount,value.observerTraceRowCount)??value.observerValue??value.afterMessageCount??value.afterValue;
return {
seq:value.seq??value.sampleSeq??value.fromSeq??value.controlSeq??null,
ts:short(value.ts??value.sampleTs??value.fromTs??value.controlTs,32),
diffKind:short(value.diffKind??value.kind??value.type??value.metric??value.anomaly,48),
control:compactMetric(controlCount),
observer:compactMetric(observerCount),
sessionId:short(value.sessionId??value.routeSessionId??value.activeSessionId??value.controlSessionId??value.observerSessionId,64),
path:short(value.path??value.urlPath??value.url??value.controlPath??value.observerPath,120),
detail:short(value.detail??(value.messageTextDigestDiff?'messageDigest '+String(value.controlMessageDigest??'-')+' != '+String(value.observerMessageDigest??'-'):undefined)??value.summary??value.message??value.preview??value.expectedPattern,180),
valuesRedacted:true
};
};
const slimFindingDetail=(parsed,id)=>{
if(!id||!parsed||typeof parsed!=='object'||!Array.isArray(parsed.findings)) return null;
const finding=parsed.findings.find((item)=>item&&typeof item==='object'&&String(item.id??item.kind??item.code??'')===id);
if(!finding) return {id,found:false,samples:[],valuesRedacted:true};
const sampleSource=Array.isArray(finding.samples)?'samples':Array.isArray(finding.groups)?'groups':Array.isArray(finding.examples)?'examples':Array.isArray(finding.rows)?'rows':'none';
const samples=firstArray(finding.samples,finding.groups,finding.examples,finding.rows).slice(0,12).map(slimFindingSample).filter(Boolean);
return {id:short(finding.id??finding.kind??finding.code,80),found:true,severity:short(finding.severity??finding.level,24),count:finding.count??finding.sampleCount??samples.length,summary:short(finding.summary??finding.message,180),sampleSource,samples,valuesRedacted:true};
};
const slimRunnerError=(item)=>item&&typeof item==='object'?{ts:short(item.ts||item.at,32),type:short(item.type,48),retry:item.retry||item.lastRetryLabel||null,retryExhausted:item.retryExhausted===true,attemptCount:item.attemptCount??(Array.isArray(item.attempts)?item.attempts.length:null),lastFailureKind:short(item.lastFailureKind||item.failureKind,80),lastReadinessReason:short(item.lastReadinessReason,80),message:short(item.message||item.lastError,240)}:null;
const slimJsonl=(value)=>{
if(!value||typeof value!=='object'||Array.isArray(value)) return {value:short(value)};
@@ -9982,9 +10041,9 @@ if(selected){
const lines=text.split(/\\r?\\n/).filter(Boolean);
if(isJsonl&&truncated&&lines.length>0) lines.shift();
let jsonSummary=null; let jsonContent=null;
if(isJson){try{const parsed=JSON.parse(raw.toString('utf8')); jsonContent=raw.length<=4096?slimJsonValue(parsed):null; jsonSummary={topLevelKeys:parsed&&typeof parsed==='object'&&!Array.isArray(parsed)?Object.keys(parsed).slice(0,24):[],counts:slimObject(parsed&&parsed.counts,16),sampleMetrics:slimObject(parsed&&parsed.sampleMetrics,16),runtimeAlerts:slimObject(parsed&&parsed.runtimeAlerts,16),pagePerformance:slimObject(parsed&&parsed.pagePerformance,16),runnerErrors:Array.isArray(parsed&&parsed.runnerErrors)?parsed.runnerErrors.slice(-4).map(slimRunnerError).filter(Boolean):[],findings:Array.isArray(parsed&&parsed.findings)?parsed.findings.slice(0,6).map(slimFinding).filter(Boolean):[]}}catch(error){jsonSummary={parseError:String(error&&error.message||error).slice(0,160)}}}
if(isJson){try{const parsed=JSON.parse(raw.toString('utf8')); jsonContent=raw.length<=4096?slimJsonValue(parsed):null; jsonSummary={topLevelKeys:parsed&&typeof parsed==='object'&&!Array.isArray(parsed)?Object.keys(parsed).slice(0,24):[],counts:slimObject(parsed&&parsed.counts,16),sampleMetrics:slimObject(parsed&&parsed.sampleMetrics,16),runtimeAlerts:slimObject(parsed&&parsed.runtimeAlerts,16),pagePerformance:slimObject(parsed&&parsed.pagePerformance,16),runnerErrors:Array.isArray(parsed&&parsed.runnerErrors)?parsed.runnerErrors.slice(-4).map(slimRunnerError).filter(Boolean):[],findings:Array.isArray(parsed&&parsed.findings)?parsed.findings.slice(0,6).map(slimFinding).filter(Boolean):[],findingDetail:slimFindingDetail(parsed,findingFilter)}}catch(error){jsonSummary={parseError:String(error&&error.message||error).slice(0,160)}}}
const jsonlTail=isJsonl?lines.slice(-8).map((line,index)=>{try{return slimJsonl(JSON.parse(line))}catch(error){return {parseError:true,index,lineTail:line.slice(-240),error:String(error&&error.message||error).slice(0,160)}}}):null;
console.log(JSON.stringify({ok:true,command:'web-probe-observe collect',stateDir:dir,mode:'file',file:{path:file,relative,byteCount:st.size,sha256:shaFile(file),truncated,content:isJsonl||isJson||jsonSummary?undefined:text.slice(0,maxTextPreviewBytes),contentTruncated:!isJsonl&&!isJson&&!jsonSummary&&text.length>maxTextPreviewBytes,jsonlTail,jsonSummary,jsonContent,lineCount:lines.length},valuesRedacted:true},null,2));
console.log(JSON.stringify({ok:true,command:'web-probe-observe collect',stateDir:dir,mode:'file',file:{path:file,relative,byteCount:st.size,sha256:shaFile(file),truncated,content:isJsonl||isJson||jsonSummary?undefined:text.slice(0,maxTextPreviewBytes),contentTruncated:!isJsonl&&!isJson&&!jsonSummary&&text.length>maxTextPreviewBytes,jsonlTail,jsonSummary,jsonContent,lineCount:lines.length},valuesRedacted:true}));
process.exit(0);
}
const out=[]; const walk=(p)=>{for(const ent of fs.readdirSync(p,{withFileTypes:true})){const full=path.join(p,ent.name); if(ent.isDirectory()) walk(full); else out.push(full); if(out.length>=maxFiles) return;}};
@@ -9992,8 +10051,8 @@ walk(dir);
const selectedFiles=out.slice(0,maxFiles);
const files=selectedFiles.slice(0,24).map(file=>{const st=fs.statSync(file); return {relative:path.relative(dir,file),byteCount:st.size,sha256:shaFile(file)}});
const totalBytes=selectedFiles.reduce((sum,file)=>sum+fs.statSync(file).size,0);
console.log(JSON.stringify({ok:true,command:'web-probe-observe collect',stateDir:dir,fileCount:selectedFiles.length,listedFileCount:files.length,omittedFileCount:Math.max(0,selectedFiles.length-files.length),totalBytes,files,valuesRedacted:true},null,2));
`)} "$state_dir" ${shellQuote(collectFile ?? "")}`;
console.log(JSON.stringify({ok:true,command:'web-probe-observe collect',stateDir:dir,fileCount:selectedFiles.length,listedFileCount:files.length,omittedFileCount:Math.max(0,selectedFiles.length-files.length),totalBytes,files,valuesRedacted:true}));
`)} "$state_dir" ${shellQuote(collectFile ?? "")} ${shellQuote(collectFinding ?? "")}`;
}
function nodeWebObserveWaitCommandShell(commandId: string, waitMs: number): string {
@@ -10048,6 +10107,13 @@ function isSafeWebObserveCollectFile(value: string): boolean {
&& /^[A-Za-z0-9_.\/-]+$/u.test(value);
}
function isSafeWebObserveFindingId(value: string): boolean {
return value.length > 0
&& value.length <= 120
&& !value.includes("\0")
&& /^[A-Za-z0-9_.:-]+$/u.test(value);
}
function isSafeWebObserveArchivePrefix(value: string): boolean {
return value.length > 0
&& value.length <= 120