fix: bound sentinel report evidence output
This commit is contained in:
@@ -30,6 +30,8 @@ bun scripts/cli.ts web-probe sentinel validate --node <node> --lane <lane> --sen
|
||||
bun scripts/cli.ts web-probe sentinel dashboard verify --node <node> --lane <lane> --sentinel <id>
|
||||
bun scripts/cli.ts web-probe sentinel dashboard screenshot --node <node> --lane <lane> --sentinel <id>
|
||||
bun scripts/cli.ts web-probe sentinel report --node <node> --lane <lane> --sentinel <id> --latest --view summary
|
||||
bun scripts/cli.ts web-probe sentinel report --node <node> --lane <lane> --sentinel <id> --latest --view summary --raw
|
||||
bun scripts/cli.ts web-probe sentinel report --node <node> --lane <lane> --sentinel <id> --latest --view summary --full
|
||||
bun scripts/cli.ts web-probe sentinel control-plane trigger-current --node <node> --lane <lane> --sentinel <id> --confirm
|
||||
trans <node>:k3s kubectl -n <namespace> get cronjob -l app.kubernetes.io/component=cadence-scheduler
|
||||
trans <node>:k3s kubectl -n <namespace> create job --from=cronjob/<quick-verify-cronjob> <manual-job-name>
|
||||
@@ -56,6 +58,7 @@ bun scripts/cli.ts web-probe observe analyze <observerId>
|
||||
6. Separate check type counts and sample counts: `findingCount`/`findingTypeCount` is a type count, while `severityCounts` and finding `count` are sample counts.
|
||||
7. Trace-frame reports should prefer latest terminal/completed samples. If a report shows an early running/non-terminal sample, check whether the frame reports a later terminal sample and rerun with that `--sample-seq` before concluding the business turn is still running.
|
||||
8. Browser memory/responsiveness/CDP red findings may include `rootCauseSignals` such as session list reads, trace event reads, web-performance beacon failures, EventSource failures and requestfailed/http TopN. Use those fields as first-line root-cause evidence for refresh storms before manually grepping JSONL artifacts.
|
||||
9. `web-probe sentinel report --raw` is the bounded issue-evidence JSON view. It should include run/report SHA, compact findings, artifact summary and `rootCauseSignalFindings` when available. Use `--full` only when the complete indexed service payload is explicitly needed.
|
||||
|
||||
## Architecture Preference
|
||||
|
||||
|
||||
@@ -66,6 +66,8 @@ export function hwlabNodeWebProbeHelp(): Record<string, unknown> {
|
||||
"bun scripts/cli.ts web-probe sentinel plan --node D601 --lane v03 --sentinel workbench-auth-session-switch-2users",
|
||||
"bun scripts/cli.ts web-probe sentinel dashboard verify --node D601 --lane v03 --sentinel workbench-dsflash-go-tool-call-10x",
|
||||
"bun scripts/cli.ts web-probe sentinel dashboard screenshot --node D601 --lane v03 --sentinel workbench-auth-session-switch-2users",
|
||||
"bun scripts/cli.ts web-probe sentinel report --node D601 --lane v03 --sentinel workbench-dsflash-go-tool-call-10x --latest --view summary --raw",
|
||||
"bun scripts/cli.ts web-probe sentinel report --node D601 --lane v03 --sentinel workbench-dsflash-go-tool-call-10x --latest --view summary --full",
|
||||
"bun scripts/cli.ts web-probe sentinel maintenance stop --node D601 --lane v03 --sentinel workbench-dsflash-go-tool-call-10x --confirm --wait --release-id <id>",
|
||||
],
|
||||
actions: {
|
||||
@@ -82,6 +84,7 @@ export function hwlabNodeWebProbeHelp(): Record<string, unknown> {
|
||||
"After observe start, prefer observe status|command|stop|collect|analyze <id> instead of repeating --node/--lane/--state-dir.",
|
||||
"collect views render bounded summaries from existing artifacts and do not create a second source of truth.",
|
||||
"analyze is offline-only: it reads artifact JSONL and writes analysis/report.md plus analysis/report.json.",
|
||||
"`web-probe sentinel report --raw` returns bounded issue evidence JSON, including report/artifact SHA and root-cause signal summaries when available; use `--full` for the full indexed service payload.",
|
||||
"When multiple web-probe sentinels are declared, sentinel image/control-plane/validate/maintenance/dashboard/report require `--sentinel <id>`; plan/status without it show the registry drill-down.",
|
||||
"Issue evidence should cite observer id, stateDir, report SHA, screenshot SHA, command ids and concise summaries, not prompt/provider/secret payloads.",
|
||||
],
|
||||
|
||||
@@ -97,6 +97,7 @@ export type WebProbeSentinelOptions =
|
||||
readonly traceId: string | null;
|
||||
readonly sampleSeq: number | null;
|
||||
readonly raw: boolean;
|
||||
readonly full: boolean;
|
||||
readonly timeoutSeconds: number;
|
||||
}
|
||||
| {
|
||||
|
||||
@@ -709,6 +709,7 @@ function compactQuickVerifyRecordFinding(value: unknown): Record<string, unknown
|
||||
rootCauseConfidence: boundQuickVerifyRecordText(item.rootCauseConfidence, 40),
|
||||
nextAction: boundQuickVerifyRecordText(item.nextAction, 240),
|
||||
evidenceSummary: stringAtNullable(item, "evidenceSummary") ?? compactQuickVerifyFindingEvidence(item.evidence),
|
||||
rootCauseSignals: compactQuickVerifyRootCauseSignals(item.rootCauseSignals),
|
||||
timingSourceOfTruth: boundQuickVerifyRecordText(item.timingSourceOfTruth ?? item.expectedElapsedSource ?? item.evidenceKind, 100),
|
||||
timingStatus: boundQuickVerifyRecordText(item.timingStatus, 60),
|
||||
timingAlert: item.timingAlert === true,
|
||||
@@ -717,6 +718,28 @@ function compactQuickVerifyRecordFinding(value: unknown): Record<string, unknown
|
||||
};
|
||||
}
|
||||
|
||||
function compactQuickVerifyRootCauseSignals(value: unknown): Record<string, unknown> | null {
|
||||
const item = record(value);
|
||||
const keys = [
|
||||
"sessionListReadCount",
|
||||
"traceEventsReadCount",
|
||||
"webPerformanceBeaconFailureCount",
|
||||
"eventSourceFailureCount",
|
||||
"requestFailedCount",
|
||||
"httpErrorCount",
|
||||
"consoleAlertCount",
|
||||
"requestfailedTop",
|
||||
"httpStatusTop",
|
||||
];
|
||||
const compact: Record<string, unknown> = {};
|
||||
for (const key of keys) {
|
||||
const raw = item[key];
|
||||
if (raw === null || raw === undefined) continue;
|
||||
compact[key] = Array.isArray(raw) ? raw.slice(0, 8).map(record) : raw;
|
||||
}
|
||||
return Object.keys(compact).length === 0 ? null : { ...compact, valuesRedacted: true };
|
||||
}
|
||||
|
||||
function compactQuickVerifyFindingEvidence(value: unknown): string | null {
|
||||
const item = record(value);
|
||||
if (Object.keys(item).length === 0) return null;
|
||||
@@ -787,11 +810,12 @@ function readAnalysisSummaryFromWorkspace(state: SentinelCicdState, stateDir: st
|
||||
"const read=(p)=>{try{return fs.readFileSync(p)}catch{return null}}; const jsonBuf=read(reportPath);",
|
||||
"const sha=(buf)=>buf?`sha256:${crypto.createHash('sha256').update(buf).digest('hex')}`:null;",
|
||||
"const rec=(v)=>v&&typeof v==='object'&&!Array.isArray(v)?v:{}; const arr=(v)=>Array.isArray(v)?v:[]; const clip=(v,n=180)=>v==null?null:String(v).slice(0,n);",
|
||||
"const compactRootCauseSignals=(value)=>{const v=rec(value); const keys=['sessionListReadCount','traceEventsReadCount','webPerformanceBeaconFailureCount','eventSourceFailureCount','requestFailedCount','httpErrorCount','consoleAlertCount','requestfailedTop','httpStatusTop']; const out={}; for(const key of keys){if(v[key]!=null)out[key]=Array.isArray(v[key])?v[key].slice(0,8):v[key];} if(Object.keys(out).length===0)return null; out.valuesRedacted=true; return out;};",
|
||||
"let report=null; try{report=jsonBuf?JSON.parse(jsonBuf.toString('utf8')):null}catch{}",
|
||||
"let artifactCount=0; let screenshot=null;",
|
||||
"function walk(dir){let entries=[]; try{entries=fs.readdirSync(dir,{withFileTypes:true})}catch{return}; for(const e of entries){const p=path.join(dir,e.name); if(e.isDirectory()) walk(p); else { artifactCount++; if(/\\.png$/i.test(e.name)){const b=read(p); screenshot={path:p,sha256:sha(b),bytes:b?b.length:0}; } } }}",
|
||||
"walk(stateDir);",
|
||||
"const findings=arr(report?.findings ?? report?.archiveSummary?.redFindings).slice(0,20).map((item)=>{const v=rec(item); return {id:clip(v.id??v.kind??v.code,80),kind:clip(v.kind??v.id??v.code,80),code:clip(v.code??v.kind??v.id,80),severity:clip(v.severity??v.level,32),level:clip(v.level??v.severity,32),count:Number(v.count??v.sampleCount??1),summary:clip(v.summary??v.message,220),message:clip(v.message??v.summary,220),rootCause:clip(v.rootCause,140),rootCauseStatus:clip(v.rootCauseStatus,90),rootCauseConfidence:clip(v.rootCauseConfidence,40),nextAction:clip(v.nextAction,240),evidenceSummary:v.evidence?clip(JSON.stringify(rec(v.evidence)),220):clip(v.evidenceSummary,220),timingSourceOfTruth:clip(v.timingSourceOfTruth??v.expectedElapsedSource??v.evidenceKind,100),timingStatus:clip(v.timingStatus,60),timingAlert:v.timingAlert===true,blocking:v.blocking===true,afterRound:v.afterRound??null,canarySessionId:clip(v.canarySessionId,80),routeSessionId:clip(v.routeSessionId,80),activeSessionId:clip(v.activeSessionId,80),consecutiveUserMessageCount:v.consecutiveUserMessageCount??null,sentinelRange:clip(v.sentinelRange,80),sampleSeq:v.sampleSeq??null,traceIds:arr(v.traceIds).slice(0,8).map((x)=>clip(x,80)),pageRole:clip(v.pageRole,32),pageId:clip(v.pageId,80),observerId:clip(v.observerId,80),stateDir:clip(v.stateDir,160),commandId:clip(v.commandId,80),valuesRedacted:true};});",
|
||||
"const findings=arr(report?.findings ?? report?.archiveSummary?.redFindings).slice(0,20).map((item)=>{const v=rec(item); return {id:clip(v.id??v.kind??v.code,80),kind:clip(v.kind??v.id??v.code,80),code:clip(v.code??v.kind??v.id,80),severity:clip(v.severity??v.level,32),level:clip(v.level??v.severity,32),count:Number(v.count??v.sampleCount??1),summary:clip(v.summary??v.message,220),message:clip(v.message??v.summary,220),rootCause:clip(v.rootCause,140),rootCauseStatus:clip(v.rootCauseStatus,90),rootCauseConfidence:clip(v.rootCauseConfidence,40),nextAction:clip(v.nextAction,240),evidenceSummary:v.evidence?clip(JSON.stringify(rec(v.evidence)),220):clip(v.evidenceSummary,220),timingSourceOfTruth:clip(v.timingSourceOfTruth??v.expectedElapsedSource??v.evidenceKind,100),timingStatus:clip(v.timingStatus,60),timingAlert:v.timingAlert===true,rootCauseSignals:compactRootCauseSignals(v.rootCauseSignals),blocking:v.blocking===true,afterRound:v.afterRound??null,canarySessionId:clip(v.canarySessionId,80),routeSessionId:clip(v.routeSessionId,80),activeSessionId:clip(v.activeSessionId,80),consecutiveUserMessageCount:v.consecutiveUserMessageCount??null,sentinelRange:clip(v.sentinelRange,80),sampleSeq:v.sampleSeq??null,traceIds:arr(v.traceIds).slice(0,8).map((x)=>clip(x,80)),pageRole:clip(v.pageRole,32),pageId:clip(v.pageId,80),observerId:clip(v.observerId,80),stateDir:clip(v.stateDir,160),commandId:clip(v.commandId,80),valuesRedacted:true};});",
|
||||
"const slow=arr(report?.pagePerformanceSlowApi ?? report?.archivePagePerformanceSlowApi).slice(0,8).map((item)=>{const v=rec(item); return {path:clip(v.path??v.route,120),sampleCount:v.sampleCount??null,p95Ms:v.p95Ms??null,maxMs:v.maxMs??null,overFiveSecondCount:v.overFiveSecondCount??null};});",
|
||||
"console.log(JSON.stringify({ok:!!report,reportOk:!!report&&report.ok!==false,stateDir,reportJsonPath:reportPath,reportJsonSha256:sha(jsonBuf),reportMdPath,reportMdSha256:sha(read(reportMdPath)),findingCount:Number(report?.findingCount??findings.length),artifactCount,screenshot,findings,counts:rec(report?.counts),analysisWindow:rec(report?.analysisWindow??report?.windows?.recent?.summary),pagePerformanceSlowApi:slow,valuesRedacted:true}));",
|
||||
"NODE",
|
||||
|
||||
@@ -208,7 +208,174 @@ export function runSentinelReport(state: SentinelCicdState, options: Extract<Web
|
||||
const body = record(report.bodyJson);
|
||||
const renderedText = typeof body.renderedText === "string" ? body.renderedText : renderReportResult({ command, node: state.spec.nodeId, lane: state.spec.lane, report, valuesRedacted: true });
|
||||
const rawPayload = Object.keys(body).length > 0 ? body : report;
|
||||
return rendered(report.ok && body.ok !== false, command, options.raw ? JSON.stringify(rawPayload, null, 2) : renderedText);
|
||||
if (options.full) return rendered(report.ok && body.ok !== false, command, JSON.stringify(rawPayload, null, 2));
|
||||
if (options.raw) {
|
||||
const artifactSummary = readSentinelReportArtifactSummary(state, body, Math.min(options.timeoutSeconds, 55));
|
||||
return rendered(report.ok && body.ok !== false, command, JSON.stringify(compactSentinelReportRawPayload(state, body, report, artifactSummary), null, 2));
|
||||
}
|
||||
return rendered(report.ok && body.ok !== false, command, renderedText);
|
||||
}
|
||||
|
||||
function compactSentinelReportRawPayload(
|
||||
state: SentinelCicdState,
|
||||
body: Record<string, unknown>,
|
||||
report: Record<string, unknown>,
|
||||
artifactSummary: Record<string, unknown> | null,
|
||||
): Record<string, unknown> {
|
||||
const run = record(body.run);
|
||||
const artifact = record(artifactSummary);
|
||||
const findings = Array.isArray(body.findings) ? body.findings.map(record) : [];
|
||||
const artifactFindings = Array.isArray(artifact.findings) ? artifact.findings.map(record) : [];
|
||||
const rootCauseSignalFindings = artifactFindings
|
||||
.filter((item) => Object.keys(record(item.rootCauseSignals)).length > 0)
|
||||
.slice(0, 8)
|
||||
.map((item) => ({
|
||||
id: stringAtNullable(item, "id") ?? stringAtNullable(item, "kind") ?? stringAtNullable(item, "code"),
|
||||
severity: stringAtNullable(item, "severity") ?? stringAtNullable(item, "level"),
|
||||
summary: reportText(item.summary ?? item.message, 220),
|
||||
rootCauseSignals: compactRootCauseSignals(item.rootCauseSignals),
|
||||
valuesRedacted: true,
|
||||
}));
|
||||
return {
|
||||
ok: body.ok !== false && report.ok !== false,
|
||||
view: body.view ?? null,
|
||||
node: state.spec.nodeId,
|
||||
lane: state.spec.lane,
|
||||
sentinelId: state.sentinelId,
|
||||
run: {
|
||||
id: run.id ?? null,
|
||||
scenarioId: run.scenario_id ?? run.scenarioId ?? null,
|
||||
status: run.status ?? null,
|
||||
observerId: run.observer_id ?? run.observerId ?? null,
|
||||
stateDir: run.state_dir ?? run.stateDir ?? null,
|
||||
reportJsonSha256: run.report_json_sha256 ?? run.reportJsonSha256 ?? artifact.reportJsonSha256 ?? null,
|
||||
findingCount: run.finding_count ?? run.findingCount ?? findings.length,
|
||||
artifactCount: run.artifact_count ?? run.artifactCount ?? artifact.artifactCount ?? null,
|
||||
updatedAt: run.updated_at ?? run.updatedAt ?? null,
|
||||
valuesRedacted: true,
|
||||
},
|
||||
summary: pickFields(record(body.summary), ["reason", "status", "businessStatus", "failure", "valuesRedacted"]),
|
||||
findings: findings.slice(0, 12).map(compactSentinelReportFinding),
|
||||
artifactSummary: Object.keys(artifact).length === 0 ? null : {
|
||||
ok: artifact.ok === true,
|
||||
reportOk: artifact.reportOk === true ? true : artifact.reportOk === false ? false : null,
|
||||
reportJsonPath: artifact.reportJsonPath ?? null,
|
||||
reportJsonSha256: artifact.reportJsonSha256 ?? null,
|
||||
reportMdSha256: artifact.reportMdSha256 ?? null,
|
||||
screenshot: record(artifact.screenshot),
|
||||
counts: record(artifact.counts),
|
||||
analysisWindow: compactSentinelAnalysisWindow(artifact.analysisWindow),
|
||||
pagePerformanceSlowApi: Array.isArray(artifact.pagePerformanceSlowApi) ? artifact.pagePerformanceSlowApi.slice(0, 8).map(record) : [],
|
||||
rootCauseSignalFindings,
|
||||
valuesRedacted: true,
|
||||
},
|
||||
next: {
|
||||
text: "Default report is bounded text; use --full for the full indexed service payload.",
|
||||
report: `bun scripts/cli.ts web-probe sentinel report --node ${state.spec.nodeId} --lane ${state.spec.lane}${sentinelCliSuffix(state)} --latest --view ${String(body.view ?? "summary")}`,
|
||||
full: `bun scripts/cli.ts web-probe sentinel report --node ${state.spec.nodeId} --lane ${state.spec.lane}${sentinelCliSuffix(state)} --latest --view ${String(body.view ?? "summary")} --full`,
|
||||
},
|
||||
valuesRedacted: true,
|
||||
};
|
||||
}
|
||||
|
||||
function compactSentinelReportFinding(value: Record<string, unknown>): Record<string, unknown> {
|
||||
const result: Record<string, unknown> = {
|
||||
id: stringAtNullable(value, "finding_id") ?? stringAtNullable(value, "findingId") ?? stringAtNullable(value, "id") ?? stringAtNullable(value, "kind") ?? stringAtNullable(value, "code"),
|
||||
severity: stringAtNullable(value, "severity") ?? stringAtNullable(value, "level"),
|
||||
count: value.count ?? null,
|
||||
summary: reportText(value.summary ?? value.message, 220),
|
||||
valuesRedacted: true,
|
||||
};
|
||||
const rootCause = stringAtNullable(value, "rootCause");
|
||||
if (rootCause !== null) result.rootCause = rootCause;
|
||||
const rootCauseStatus = stringAtNullable(value, "rootCauseStatus");
|
||||
if (rootCauseStatus !== null) result.rootCauseStatus = rootCauseStatus;
|
||||
const rootCauseConfidence = stringAtNullable(value, "rootCauseConfidence");
|
||||
if (rootCauseConfidence !== null) result.rootCauseConfidence = rootCauseConfidence;
|
||||
const nextAction = reportText(value.nextAction, 240);
|
||||
if (nextAction !== null) result.nextAction = nextAction;
|
||||
const evidenceSummary = reportText(value.evidenceSummary, 240);
|
||||
if (evidenceSummary !== null) result.evidenceSummary = evidenceSummary;
|
||||
const timingSourceOfTruth = stringAtNullable(value, "timingSourceOfTruth");
|
||||
if (timingSourceOfTruth !== null) result.timingSourceOfTruth = timingSourceOfTruth;
|
||||
const timingStatus = stringAtNullable(value, "timingStatus");
|
||||
if (timingStatus !== null) result.timingStatus = timingStatus;
|
||||
if (value.timingAlert === true) result.timingAlert = true;
|
||||
const rootCauseSignals = compactRootCauseSignals(value.rootCauseSignals);
|
||||
if (rootCauseSignals !== null) result.rootCauseSignals = rootCauseSignals;
|
||||
const check = record(value.check);
|
||||
const checkCode = stringAtNullable(check, "code");
|
||||
const checkTitle = stringAtNullable(check, "titleZh");
|
||||
if (checkCode !== null || checkTitle !== null) {
|
||||
result.check = pickFields(check, ["id", "code", "level", "titleZh", "blocking", "registered"]);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function readSentinelReportArtifactSummary(state: SentinelCicdState, body: Record<string, unknown>, timeoutSeconds: number): Record<string, unknown> | null {
|
||||
const run = record(body.run);
|
||||
const stateDir = stringAtNullable(run, "state_dir") ?? stringAtNullable(run, "stateDir");
|
||||
if (stateDir === null || !isSafeSentinelReportStateDir(stateDir)) return null;
|
||||
const script = [
|
||||
"set -eu",
|
||||
`state_dir=${shellQuote(stateDir)}`,
|
||||
"node - \"$state_dir\" <<'NODE'",
|
||||
"const fs=require('node:fs'); const path=require('node:path'); const crypto=require('node:crypto');",
|
||||
"const stateDir=process.argv[2]; const reportPath=path.join(stateDir,'analysis','report.json'); const reportMdPath=path.join(stateDir,'analysis','report.md');",
|
||||
"const read=(p)=>{try{return fs.readFileSync(p)}catch{return null}}; const jsonBuf=read(reportPath);",
|
||||
"const sha=(buf)=>buf?`sha256:${crypto.createHash('sha256').update(buf).digest('hex')}`:null;",
|
||||
"const rec=(v)=>v&&typeof v==='object'&&!Array.isArray(v)?v:{}; const arr=(v)=>Array.isArray(v)?v:[]; const clip=(v,n=180)=>v==null?null:String(v).slice(0,n);",
|
||||
"const compactRootCauseSignals=(value)=>{const v=rec(value); const keys=['sessionListReadCount','traceEventsReadCount','webPerformanceBeaconFailureCount','eventSourceFailureCount','requestFailedCount','httpErrorCount','consoleAlertCount','requestfailedTop','httpStatusTop']; const out={}; for(const key of keys){if(v[key]!=null)out[key]=Array.isArray(v[key])?v[key].slice(0,8):v[key];} if(Object.keys(out).length===0)return null; out.valuesRedacted=true; return out;};",
|
||||
"let report=null; try{report=jsonBuf?JSON.parse(jsonBuf.toString('utf8')):null}catch{}",
|
||||
"let artifactCount=0; let screenshot=null;",
|
||||
"function walk(dir){let entries=[]; try{entries=fs.readdirSync(dir,{withFileTypes:true})}catch{return}; for(const e of entries){const p=path.join(dir,e.name); if(e.isDirectory()) walk(p); else { artifactCount++; if(/\\.png$/i.test(e.name)){const b=read(p); screenshot={path:p,sha256:sha(b),bytes:b?b.length:0}; } } }}",
|
||||
"walk(stateDir);",
|
||||
"const findings=arr(report?.findings ?? report?.archiveSummary?.redFindings).slice(0,20).map((item)=>{const v=rec(item); return {id:clip(v.id??v.kind??v.code,80),kind:clip(v.kind??v.id??v.code,80),code:clip(v.code??v.kind??v.id,80),severity:clip(v.severity??v.level,32),level:clip(v.level??v.severity,32),count:Number(v.count??v.sampleCount??1),summary:clip(v.summary??v.message,220),message:clip(v.message??v.summary,220),rootCause:clip(v.rootCause,140),rootCauseStatus:clip(v.rootCauseStatus,90),rootCauseConfidence:clip(v.rootCauseConfidence,40),nextAction:clip(v.nextAction,240),evidenceSummary:v.evidence?clip(JSON.stringify(rec(v.evidence)),220):clip(v.evidenceSummary,220),timingSourceOfTruth:clip(v.timingSourceOfTruth??v.expectedElapsedSource??v.evidenceKind,100),timingStatus:clip(v.timingStatus,60),timingAlert:v.timingAlert===true,rootCauseSignals:compactRootCauseSignals(v.rootCauseSignals),blocking:v.blocking===true,afterRound:v.afterRound??null,canarySessionId:clip(v.canarySessionId,80),routeSessionId:clip(v.routeSessionId,80),activeSessionId:clip(v.activeSessionId,80),consecutiveUserMessageCount:v.consecutiveUserMessageCount??null,sentinelRange:clip(v.sentinelRange,80),sampleSeq:v.sampleSeq??null,traceIds:arr(v.traceIds).slice(0,8).map((x)=>clip(x,80)),pageRole:clip(v.pageRole,32),pageId:clip(v.pageId,80),observerId:clip(v.observerId,80),stateDir:clip(v.stateDir,160),commandId:clip(v.commandId,80),valuesRedacted:true};});",
|
||||
"const slow=arr(report?.pagePerformanceSlowApi ?? report?.archivePagePerformanceSlowApi).slice(0,8).map((item)=>{const v=rec(item); return {path:clip(v.path??v.route,120),sampleCount:v.sampleCount??null,p95Ms:v.p95Ms??null,maxMs:v.maxMs??null,overFiveSecondCount:v.overFiveSecondCount??null};});",
|
||||
"console.log(JSON.stringify({ok:!!report,reportOk:!!report&&report.ok!==false,stateDir,reportJsonPath:reportPath,reportJsonSha256:sha(jsonBuf),reportMdPath,reportMdSha256:sha(read(reportMdPath)),findingCount:Number(report?.findingCount??findings.length),artifactCount,screenshot,findings,counts:rec(report?.counts),analysisWindow:rec(report?.analysisWindow??report?.windows?.recent?.summary),pagePerformanceSlowApi:slow,valuesRedacted:true}));",
|
||||
"NODE",
|
||||
].join("\n");
|
||||
const result = runCommand(["trans", `${state.spec.nodeId}:${state.spec.workspace}`, "sh"], repoRoot, { input: script, timeoutMs: Math.max(5, Math.min(timeoutSeconds, 55)) * 1000 });
|
||||
const parsed = parseJsonObject(result.stdout);
|
||||
return { ok: result.exitCode === 0 && parsed?.ok === true, ...record(parsed), result: compactCommand(result), valuesRedacted: true };
|
||||
}
|
||||
|
||||
function isSafeSentinelReportStateDir(value: string): boolean {
|
||||
return value.startsWith(".state/web-observe/") && !value.includes("\0") && !value.includes("..") && !value.startsWith("/");
|
||||
}
|
||||
|
||||
function compactRootCauseSignals(value: unknown): Record<string, unknown> | null {
|
||||
const item = record(value);
|
||||
const keys = [
|
||||
"sessionListReadCount",
|
||||
"traceEventsReadCount",
|
||||
"webPerformanceBeaconFailureCount",
|
||||
"eventSourceFailureCount",
|
||||
"requestFailedCount",
|
||||
"httpErrorCount",
|
||||
"consoleAlertCount",
|
||||
"requestfailedTop",
|
||||
"httpStatusTop",
|
||||
];
|
||||
const out: Record<string, unknown> = {};
|
||||
for (const key of keys) {
|
||||
const raw = item[key];
|
||||
if (raw === null || raw === undefined) continue;
|
||||
out[key] = Array.isArray(raw) ? raw.slice(0, 8).map(record) : raw;
|
||||
}
|
||||
return Object.keys(out).length === 0 ? null : { ...out, valuesRedacted: true };
|
||||
}
|
||||
|
||||
function compactSentinelAnalysisWindow(value: unknown): Record<string, unknown> | null {
|
||||
const item = record(value);
|
||||
if (Object.keys(item).length === 0) return null;
|
||||
return pickFields(item, ["name", "windowMs", "samples", "control", "network", "console", "valuesRedacted"]);
|
||||
}
|
||||
|
||||
function reportText(value: unknown, maxChars: number): string | null {
|
||||
if (value === undefined || value === null || value === "") return null;
|
||||
const raw = text(value);
|
||||
return raw.length <= maxChars ? raw : `${raw.slice(0, Math.max(0, maxChars - 1))}…`;
|
||||
}
|
||||
|
||||
export function runSentinelDashboard(state: SentinelCicdState, options: Extract<WebProbeSentinelOptions, { kind: "dashboard" }>): RenderedCliResult {
|
||||
|
||||
@@ -1150,6 +1150,7 @@ function enrichFindingRowWithStoredDetail(config: WebProbeSentinelServiceConfig,
|
||||
rootCauseConfidence: stringOrNull(detail?.rootCauseConfidence),
|
||||
nextAction: stringOrNull(detail?.nextAction),
|
||||
evidenceSummary: stringOrNull(detail?.evidenceSummary),
|
||||
rootCauseSignals: record(detail?.rootCauseSignals),
|
||||
timingSourceOfTruth: stringOrNull(detail?.timingSourceOfTruth),
|
||||
timingStatus: stringOrNull(detail?.timingStatus),
|
||||
timingAlert: detail?.timingAlert === true,
|
||||
@@ -1190,6 +1191,7 @@ function compactStoredFinding(value: unknown): Record<string, unknown> {
|
||||
rootCauseConfidence: stringOrNull(item.rootCauseConfidence),
|
||||
nextAction: stringOrNull(item.nextAction),
|
||||
evidenceSummary: stringOrNull(item.evidenceSummary) ?? compactFindingEvidenceSummary(item.evidence),
|
||||
rootCauseSignals: compactFindingRootCauseSignals(item.rootCauseSignals),
|
||||
timingSourceOfTruth: stringOrNull(item.timingSourceOfTruth) ?? stringOrNull(item.expectedElapsedSource) ?? stringOrNull(item.evidenceKind),
|
||||
timingStatus: stringOrNull(item.timingStatus),
|
||||
timingAlert: item.timingAlert === true,
|
||||
@@ -1197,6 +1199,28 @@ function compactStoredFinding(value: unknown): Record<string, unknown> {
|
||||
};
|
||||
}
|
||||
|
||||
function compactFindingRootCauseSignals(value: unknown): Record<string, unknown> | null {
|
||||
const item = record(value);
|
||||
const keys = [
|
||||
"sessionListReadCount",
|
||||
"traceEventsReadCount",
|
||||
"webPerformanceBeaconFailureCount",
|
||||
"eventSourceFailureCount",
|
||||
"requestFailedCount",
|
||||
"httpErrorCount",
|
||||
"consoleAlertCount",
|
||||
"requestfailedTop",
|
||||
"httpStatusTop",
|
||||
];
|
||||
const compact: Record<string, unknown> = {};
|
||||
for (const key of keys) {
|
||||
const raw = item[key];
|
||||
if (raw === null || raw === undefined) continue;
|
||||
compact[key] = Array.isArray(raw) ? raw.slice(0, 8).map(record) : raw;
|
||||
}
|
||||
return Object.keys(compact).length === 0 ? null : { ...compact, valuesRedacted: true };
|
||||
}
|
||||
|
||||
function compactFindingEvidenceSummary(value: unknown): string | null {
|
||||
if (value === null || value === undefined) return null;
|
||||
if (typeof value === "string") return value.slice(0, 240);
|
||||
|
||||
@@ -71,7 +71,7 @@ export function parseNodeWebProbeSentinelOptions(args: string[]): NodeWebProbeSe
|
||||
"--timeout-ms",
|
||||
"--wait-timeout-ms",
|
||||
"--command-timeout-seconds",
|
||||
]), new Set(["--dry-run", "--confirm", "--wait", "--quick-verify", "--raw", "--latest", "--full-page", "--no-full-page"]));
|
||||
]), new Set(["--dry-run", "--confirm", "--wait", "--quick-verify", "--raw", "--full", "--latest", "--full-page", "--no-full-page"]));
|
||||
const node = requiredOption(args, "--node");
|
||||
assertNodeId(node);
|
||||
const lane = requiredOption(args, "--lane");
|
||||
@@ -158,6 +158,7 @@ export function parseNodeWebProbeSentinelOptions(args: string[]): NodeWebProbeSe
|
||||
traceId: optionValue(args, "--trace-id") ?? null,
|
||||
sampleSeq,
|
||||
raw: args.includes("--raw"),
|
||||
full: args.includes("--full"),
|
||||
timeoutSeconds,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user