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; tailLines: number;
maxFiles: number; maxFiles: number;
collectFile: string | null; collectFile: string | null;
collectFinding: string | null;
analyzeArchivePrefix: string | null; analyzeArchivePrefix: string | null;
full: boolean; full: boolean;
stateDir: string | null; stateDir: string | null;
@@ -7266,6 +7267,7 @@ function parseNodeWebProbeObserveOptions(
"--tail-lines", "--tail-lines",
"--max-files", "--max-files",
"--file", "--file",
"--finding",
"--archive-prefix", "--archive-prefix",
"--state-dir", "--state-dir",
"--job-id", "--job-id",
@@ -7284,6 +7286,8 @@ function parseNodeWebProbeObserveOptions(
if (jobId !== null && !isSafeWebObserveJobId(jobId)) throw new Error(`unsafe web-probe observe --job-id: ${jobId}`); if (jobId !== null && !isSafeWebObserveJobId(jobId)) throw new Error(`unsafe web-probe observe --job-id: ${jobId}`);
const collectFile = optionValue(args, "--file") ?? null; const collectFile = optionValue(args, "--file") ?? null;
if (collectFile !== null && !isSafeWebObserveCollectFile(collectFile)) throw new Error(`unsafe web-probe observe --file: ${collectFile}`); 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; const analyzeArchivePrefix = optionValue(args, "--archive-prefix") ?? null;
if (analyzeArchivePrefix !== null && !isSafeWebObserveArchivePrefix(analyzeArchivePrefix)) throw new Error(`unsafe web-probe observe --archive-prefix: ${analyzeArchivePrefix}`); if (analyzeArchivePrefix !== null && !isSafeWebObserveArchivePrefix(analyzeArchivePrefix)) throw new Error(`unsafe web-probe observe --archive-prefix: ${analyzeArchivePrefix}`);
if (observeActionRaw !== "start" && stateDir === null && jobId === null) { if (observeActionRaw !== "start" && stateDir === null && jobId === null) {
@@ -7317,6 +7321,7 @@ function parseNodeWebProbeObserveOptions(
tailLines: positiveIntegerOption(args, "--tail-lines", 5, 200), tailLines: positiveIntegerOption(args, "--tail-lines", 5, 200),
maxFiles: positiveIntegerOption(args, "--max-files", 80, 5000), maxFiles: positiveIntegerOption(args, "--max-files", 80, 5000),
collectFile, collectFile,
collectFinding,
analyzeArchivePrefix, analyzeArchivePrefix,
full: args.includes("--full"), full: args.includes("--full"),
stateDir, stateDir,
@@ -8188,7 +8193,7 @@ function runNodeWebProbeObserveCollect(options: NodeWebProbeObserveOptions, spec
const script = [ const script = [
"set -eu", "set -eu",
nodeWebObserveResolveStateDirShell(options), nodeWebObserveResolveStateDirShell(options),
nodeWebObserveCollectNodeScript(options.maxFiles, options.collectFile), nodeWebObserveCollectNodeScript(options.maxFiles, options.collectFile, options.collectFinding),
].join("\n"); ].join("\n");
const result = runTransWorkspaceStdinScript(options.node, spec.workspace, script, options.commandTimeoutSeconds); const result = runTransWorkspaceStdinScript(options.node, spec.workspace, script, options.commandTimeoutSeconds);
const collect = parseJsonObject(result.stdout); 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 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 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 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 = [ const lines = [
`hwlab nodes web-probe observe collect (${webObserveText(value.status)})`, `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) { if (jsonlTail.length > 0) {
lines.push( lines.push(
"JSONL tail:", "JSONL tail:",
@@ -9894,10 +9926,10 @@ console.log(JSON.stringify({ok:true,command:'web-probe-observe status',stateDir:
`)} "$state_dir" ${shellQuote(node)} ${shellQuote(lane)}`; `)} "$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(` return `node -e ${shellQuote(`
const fs=require('fs'),path=require('path'),crypto=require('crypto'); 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 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 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); 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 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 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)=>{ const slimJsonl=(value)=>{
if(!value||typeof value!=='object'||Array.isArray(value)) return {value:short(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); const lines=text.split(/\\r?\\n/).filter(Boolean);
if(isJsonl&&truncated&&lines.length>0) lines.shift(); if(isJsonl&&truncated&&lines.length>0) lines.shift();
let jsonSummary=null; let jsonContent=null; 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; 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); 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;}}; 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 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 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); 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)); 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 ?? "")}`; `)} "$state_dir" ${shellQuote(collectFile ?? "")} ${shellQuote(collectFinding ?? "")}`;
} }
function nodeWebObserveWaitCommandShell(commandId: string, waitMs: number): string { function nodeWebObserveWaitCommandShell(commandId: string, waitMs: number): string {
@@ -10048,6 +10107,13 @@ function isSafeWebObserveCollectFile(value: string): boolean {
&& /^[A-Za-z0-9_.\/-]+$/u.test(value); && /^[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 { function isSafeWebObserveArchivePrefix(value: string): boolean {
return value.length > 0 return value.length > 0
&& value.length <= 120 && value.length <= 120
@@ -1541,6 +1541,42 @@ async function sampleOnePage(targetPage, { reason, groupSeq, pageRole, targetPag
const title = element.getAttribute("title") || ""; const title = element.getAttribute("title") || "";
return tag === "button" || role === "button" || //u.test(aria) || //u.test(title); return tag === "button" || role === "button" || //u.test(aria) || //u.test(title);
}; };
const stableMessageText = (element) => {
const bodySelectors = [
".message-markdown.message-text",
".message-text",
"[data-message-body]",
"[data-testid='message-body']",
"[data-testid*='message-text' i]",
"[data-testid*='final-response' i]"
];
const parts = [];
for (const selector of bodySelectors) {
for (const candidate of Array.from(element.querySelectorAll(selector))) {
if (!visible(candidate)) continue;
const text = trim(candidate.textContent || "", 1200);
if (text && !parts.includes(text)) parts.push(text);
}
}
if (parts.length > 0) return parts.join(" ");
const clone = element.cloneNode(true);
for (const selector of [
".message-duration-meta",
".message-activity-meta",
".api-error-diagnostic",
"[class*='diagnostic' i]",
"[class*='trace' i]",
"[data-trace-id]",
"[data-testid*='trace' i]",
"[data-testid*='event' i]",
"[role='status']",
"[role='alert']",
"button"
]) {
for (const child of Array.from(clone.querySelectorAll(selector))) child.remove();
}
return trim(clone.textContent || "", 1200);
};
const summarize = (selector, limit) => Array.from(document.querySelectorAll(selector)).filter(visible).slice(-limit).map((element, index) => { const summarize = (selector, limit) => Array.from(document.querySelectorAll(selector)).filter(visible).slice(-limit).map((element, index) => {
const rect = element.getBoundingClientRect(); const rect = element.getBoundingClientRect();
return { return {
@@ -1557,6 +1593,22 @@ async function sampleOnePage(targetPage, { reason, groupSeq, pageRole, targetPag
rect: { x: Math.round(rect.x), y: Math.round(rect.y), width: Math.round(rect.width), height: Math.round(rect.height) }, rect: { x: Math.round(rect.x), y: Math.round(rect.y), width: Math.round(rect.width), height: Math.round(rect.height) },
}; };
}); });
const summarizeMessages = (selector, limit) => Array.from(document.querySelectorAll(selector)).filter(visible).slice(-limit).map((element, index) => {
const rect = element.getBoundingClientRect();
return {
index,
tag: element.tagName.toLowerCase(),
testId: element.getAttribute("data-testid"),
role: element.getAttribute("role"),
status: element.getAttribute("data-status") || element.getAttribute("aria-busy") || null,
sessionId: element.getAttribute("data-session-id") || null,
messageId: element.getAttribute("data-message-id") || element.getAttribute("id") || null,
traceId: element.getAttribute("data-trace-id") || null,
turnId: element.getAttribute("data-turn-id") || null,
text: stableMessageText(element),
rect: { x: Math.round(rect.x), y: Math.round(rect.y), width: Math.round(rect.width), height: Math.round(rect.height) },
};
});
const resourceTimingSample = (entry) => ({ const resourceTimingSample = (entry) => ({
name: entry.name.split(/[?#]/u)[0].slice(0, 240), name: entry.name.split(/[?#]/u)[0].slice(0, 240),
initiatorType: entry.initiatorType, initiatorType: entry.initiatorType,
@@ -1592,7 +1644,7 @@ async function sampleOnePage(targetPage, { reason, groupSeq, pageRole, targetPag
const messageSelector = 'article.message-card, .message-card[data-message-id], article[data-message-id]'; const messageSelector = 'article.message-card, .message-card[data-message-id], article[data-message-id]';
const traceSelector = '[data-testid*="trace" i], [class*="trace" i], [data-trace-id], [data-testid*="event" i]'; const traceSelector = '[data-testid*="trace" i], [class*="trace" i], [data-trace-id], [data-testid*="event" i]';
const diagnosticSelector = '.api-error-diagnostic, [class*="api-error-diagnostic" i], [class*="message-diagnostic" i], [class*="projection-diagnostic" i], [data-testid="api-error-diagnostic" i], [data-testid="error-diagnostic" i], [data-testid*="diagnostic" i], [role="alert"], [aria-live="assertive"]'; const diagnosticSelector = '.api-error-diagnostic, [class*="api-error-diagnostic" i], [class*="message-diagnostic" i], [class*="projection-diagnostic" i], [data-testid="api-error-diagnostic" i], [data-testid="error-diagnostic" i], [data-testid*="diagnostic" i], [role="alert"], [aria-live="assertive"]';
const messages = summarize(messageSelector, 20); const messages = summarizeMessages(messageSelector, 20);
const traceRows = summarize(traceSelector, 30); const traceRows = summarize(traceSelector, 30);
const loadings = collectLoadingNodes(); const loadings = collectLoadingNodes();
const sessionRail = collectSessionRailTitles(); const sessionRail = collectSessionRailTitles();
@@ -4714,8 +4766,11 @@ function detectCrossPageProjectionDiffs(samples) {
const controlZero = detectTerminalZeroElapsed([control]).length > 0; const controlZero = detectTerminalZeroElapsed([control]).length > 0;
const observerZero = detectTerminalZeroElapsed([observer]).length > 0; const observerZero = detectTerminalZeroElapsed([observer]).length > 0;
const sameSession = (control.routeSessionId || control.activeSessionId || null) && (control.routeSessionId || control.activeSessionId || null) === (observer.routeSessionId || observer.activeSessionId || null); const sameSession = (control.routeSessionId || control.activeSessionId || null) && (control.routeSessionId || control.activeSessionId || null) === (observer.routeSessionId || observer.activeSessionId || null);
const projectionDivergent = sameSession && (missingInObserver.length > 0 || Math.abs(controlMessages - observerMessages) > 0 || controlZero !== observerZero || digestMessageTexts(control) !== digestMessageTexts(observer)); const controlMessageDigest = digestMessageTexts(control);
const traceVisibilityDivergent = sameSession && !projectionDivergent && Math.abs(controlTraceRows - observerTraceRows) > 0; const observerMessageDigest = digestMessageTexts(observer);
const messageTextDigestDiff = controlMessageDigest !== observerMessageDigest;
const projectionDivergent = sameSession && (Math.abs(controlMessages - observerMessages) > 0 || controlZero !== observerZero || messageTextDigestDiff);
const traceVisibilityDivergent = sameSession && !projectionDivergent && (missingInObserver.length > 0 || Math.abs(controlTraceRows - observerTraceRows) > 0);
if (!projectionDivergent && !traceVisibilityDivergent) continue; if (!projectionDivergent && !traceVisibilityDivergent) continue;
rows.push({ rows.push({
sampleGroupSeq, sampleGroupSeq,
@@ -4727,7 +4782,10 @@ function detectCrossPageProjectionDiffs(samples) {
observerMessageCount: observerMessages, observerMessageCount: observerMessages,
controlTraceRowCount: controlTraceRows, controlTraceRowCount: controlTraceRows,
observerTraceRowCount: observerTraceRows, observerTraceRowCount: observerTraceRows,
terminalZeroElapsedDiff: controlZero !== observerZero terminalZeroElapsedDiff: controlZero !== observerZero,
messageTextDigestDiff,
controlMessageDigest,
observerMessageDigest
}); });
} }
return rows; return rows;
@@ -4779,8 +4837,11 @@ function detectAdjacentCrossPageProjectionDiffs(samples) {
const controlZero = detectTerminalZeroElapsed([control]).length > 0; const controlZero = detectTerminalZeroElapsed([control]).length > 0;
const observerZero = detectTerminalZeroElapsed([observer]).length > 0; const observerZero = detectTerminalZeroElapsed([observer]).length > 0;
const missingInObserver = [...visibleTraceIds(control)].filter((item) => !visibleTraceIds(observer).has(item)); const missingInObserver = [...visibleTraceIds(control)].filter((item) => !visibleTraceIds(observer).has(item));
const projectionDivergent = missingInObserver.length > 0 || controlMessages !== observerMessages || controlZero !== observerZero || digestMessageTexts(control) !== digestMessageTexts(observer); const controlMessageDigest = digestMessageTexts(control);
const traceVisibilityDivergent = !projectionDivergent && controlTraceRows !== observerTraceRows; const observerMessageDigest = digestMessageTexts(observer);
const messageTextDigestDiff = controlMessageDigest !== observerMessageDigest;
const projectionDivergent = controlMessages !== observerMessages || controlZero !== observerZero || messageTextDigestDiff;
const traceVisibilityDivergent = !projectionDivergent && (missingInObserver.length > 0 || controlTraceRows !== observerTraceRows);
if (!projectionDivergent && !traceVisibilityDivergent) continue; if (!projectionDivergent && !traceVisibilityDivergent) continue;
rows.push({ rows.push({
sampleGroupSeq: control.sampleGroupSeq ?? observer.sampleGroupSeq ?? null, sampleGroupSeq: control.sampleGroupSeq ?? observer.sampleGroupSeq ?? null,
@@ -4793,7 +4854,10 @@ function detectAdjacentCrossPageProjectionDiffs(samples) {
observerMessageCount: observerMessages, observerMessageCount: observerMessages,
controlTraceRowCount: controlTraceRows, controlTraceRowCount: controlTraceRows,
observerTraceRowCount: observerTraceRows, observerTraceRowCount: observerTraceRows,
terminalZeroElapsedDiff: controlZero !== observerZero terminalZeroElapsedDiff: controlZero !== observerZero,
messageTextDigestDiff,
controlMessageDigest,
observerMessageDigest
}); });
} }
return rows; return rows;