fix: add web-probe finding drilldown (#733)
Co-authored-by: Codex <codex@noreply.local>
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user