feat: add project-management web-probe observe
This commit is contained in:
@@ -19,6 +19,7 @@ const analyzeTailSamples = (() => {
|
||||
return Number.isFinite(parsed) && parsed >= 0 ? Math.floor(parsed) : 360;
|
||||
})();
|
||||
const alertThresholds = parseAlertThresholds(process.env.UNIDESK_WEB_OBSERVE_ALERT_THRESHOLDS_JSON);
|
||||
const projectManagementConfig = parseProjectManagementConfig(process.env.UNIDESK_WEB_OBSERVE_PROJECT_MANAGEMENT_JSON);
|
||||
const dataDir = archivePrefix ? path.join(stateDir, "archive") : stateDir;
|
||||
const dataFile = (name) => path.join(dataDir, archivePrefix ? archivePrefix + "-" + name : name);
|
||||
const analysisDir = path.join(stateDir, "analysis");
|
||||
@@ -53,6 +54,7 @@ const pageProvenance = buildPageProvenanceReport(samples, control, manifest);
|
||||
const pagePerformance = buildPagePerformanceReport(samples, manifest);
|
||||
const promptNetwork = buildPromptNetworkReport(control, promptNetworkRows);
|
||||
const runtimeAlerts = buildRuntimeAlerts(samples, control, network, consoleEvents, errors);
|
||||
const projectManagement = buildProjectManagementReport(samples, control, network, pagePerformance, projectManagementConfig);
|
||||
const runnerErrors = errors.slice(-8).map((item) => {
|
||||
const attempts = Array.isArray(item.error?.attempts) ? item.error.attempts : [];
|
||||
const lastAttempt = attempts.length > 0 ? attempts[attempts.length - 1] : null;
|
||||
@@ -95,7 +97,7 @@ const runnerErrors = errors.slice(-8).map((item) => {
|
||||
});
|
||||
const commandFailures = summarizeCommandFailures(control);
|
||||
const toolFindings = buildToolFindings({ manifest, heartbeat, commandState });
|
||||
const findings = [...toolFindings, ...buildFindings(samples, control, network, errors, sampleMetrics, promptNetwork, runtimeAlerts, pagePerformance, pageProvenance, commandFailures)];
|
||||
const findings = [...toolFindings, ...buildProjectManagementFindings(projectManagement), ...buildFindings(samples, control, network, errors, sampleMetrics, promptNetwork, runtimeAlerts, pagePerformance, pageProvenance, commandFailures)];
|
||||
if (jsonlReadIssues.length > 0) findings.unshift({ id: "jsonl-read-issues", severity: "red", summary: "observer analyzer hit JSONL read/parse issues", count: jsonlReadIssues.length, issues: jsonlReadIssues.slice(0, 20) });
|
||||
const recentWindow = buildRecentAnalysisWindow({ samples, control, network, consoleEvents, errors, manifest });
|
||||
const commandTimeline = control.filter((item) => item.phase === "completed" || item.phase === "failed").map((item) => ({ ts: item.ts, phase: item.phase, commandId: item.commandId, type: item.type, input: item.input, afterUrl: item.afterUrl }));
|
||||
@@ -114,6 +116,7 @@ const report = {
|
||||
sampleMetrics,
|
||||
pageProvenance,
|
||||
pagePerformance,
|
||||
projectManagement,
|
||||
promptNetwork,
|
||||
runtimeAlerts,
|
||||
runnerErrors,
|
||||
@@ -142,6 +145,7 @@ console.log(JSON.stringify({
|
||||
archiveSummary: {
|
||||
sampleMetrics: sampleMetrics.summary,
|
||||
pagePerformance: pagePerformance.summary,
|
||||
projectManagement: projectManagement.summary,
|
||||
runtimeAlerts: runtimeAlerts.summary,
|
||||
findingCount: findings.length,
|
||||
redFindingCount: findings.filter((item) => item.severity === "red").length,
|
||||
@@ -159,6 +163,7 @@ console.log(JSON.stringify({
|
||||
},
|
||||
pageProvenance: recentWindow.pageProvenance.summary,
|
||||
pagePerformance: recentWindow.pagePerformance.summary,
|
||||
projectManagement: compactProjectManagementForOutput(projectManagement),
|
||||
promptNetwork: recentWindow.promptNetwork.summary,
|
||||
runtimeAlerts: recentWindow.runtimeAlerts.summary,
|
||||
runnerErrors,
|
||||
@@ -538,6 +543,47 @@ function parseAlertThresholds(value) {
|
||||
};
|
||||
}
|
||||
|
||||
function parseProjectManagementConfig(value) {
|
||||
if (!value || value === "null") {
|
||||
return {
|
||||
enabled: false,
|
||||
targetPaths: [],
|
||||
readinessSelectors: [],
|
||||
naturalApiPathPrefixes: [],
|
||||
commandAllowlist: [],
|
||||
launchRoute: "",
|
||||
slowApiBudgetMs: 0,
|
||||
source: "yaml-env",
|
||||
valuesRedacted: true
|
||||
};
|
||||
}
|
||||
const raw = (() => {
|
||||
try { return JSON.parse(value); } catch (error) { throw new Error("UNIDESK_WEB_OBSERVE_PROJECT_MANAGEMENT_JSON is invalid JSON: " + (error instanceof Error ? error.message : String(error))); }
|
||||
})();
|
||||
if (raw?.enabled !== true && raw?.enabled !== false) throw new Error("UNIDESK_WEB_OBSERVE_PROJECT_MANAGEMENT_JSON requires boolean enabled");
|
||||
if (raw.enabled !== true) return { enabled: false, targetPaths: [], readinessSelectors: [], naturalApiPathPrefixes: [], commandAllowlist: [], launchRoute: "", slowApiBudgetMs: 0, source: "yaml-env", valuesRedacted: true };
|
||||
const stringList = (key) => {
|
||||
const list = raw?.[key];
|
||||
if (!Array.isArray(list) || list.some((item) => typeof item !== "string" || item.length === 0)) throw new Error("UNIDESK_WEB_OBSERVE_PROJECT_MANAGEMENT_JSON requires string[] " + key + "; configure config/hwlab-node-lanes.yaml webProbe.projectManagement");
|
||||
return list;
|
||||
};
|
||||
const slowApiBudgetMs = Number(raw?.slowApiBudgetMs);
|
||||
if (!Number.isFinite(slowApiBudgetMs) || slowApiBudgetMs <= 0) throw new Error("UNIDESK_WEB_OBSERVE_PROJECT_MANAGEMENT_JSON requires positive slowApiBudgetMs");
|
||||
const launchRoute = String(raw.launchRoute || "");
|
||||
if (!launchRoute.startsWith("/")) throw new Error("UNIDESK_WEB_OBSERVE_PROJECT_MANAGEMENT_JSON launchRoute must be an absolute path");
|
||||
return {
|
||||
enabled: true,
|
||||
targetPaths: stringList("targetPaths"),
|
||||
readinessSelectors: stringList("readinessSelectors"),
|
||||
naturalApiPathPrefixes: stringList("naturalApiPathPrefixes"),
|
||||
commandAllowlist: stringList("commandAllowlist"),
|
||||
launchRoute,
|
||||
slowApiBudgetMs,
|
||||
source: "yaml-env",
|
||||
valuesRedacted: true
|
||||
};
|
||||
}
|
||||
|
||||
async function readJsonl(file, options = {}) {
|
||||
const tailLimit = Number.isFinite(Number(options.tail)) && Number(options.tail) > 0 ? Math.floor(Number(options.tail)) : 0;
|
||||
if (tailLimit > 0) return readJsonlTail(file, tailLimit, options);
|
||||
@@ -712,11 +758,38 @@ function compactSampleForAnalysis(sample) {
|
||||
sessionRail: compactSessionRail(sample.sessionRail),
|
||||
turns: compactDomItems(sample.turns),
|
||||
diagnostics: compactDomItems(sample.diagnostics),
|
||||
projectManagement: compactProjectManagementSample(sample.projectManagement),
|
||||
pageProvenance: compactSamplePageProvenance(sample.pageProvenance),
|
||||
performance: compactPerformanceItems(sample.performance)
|
||||
};
|
||||
}
|
||||
|
||||
function compactProjectManagementSample(value) {
|
||||
if (!value || typeof value !== "object") return null;
|
||||
return {
|
||||
pageKind: value.pageKind ?? null,
|
||||
configuredPath: value.configuredPath === true,
|
||||
rootVisible: value.rootVisible === true,
|
||||
mdtodoVisible: value.mdtodoVisible === true,
|
||||
sourceCount: value.sourceCount ?? null,
|
||||
fileCount: value.fileCount ?? null,
|
||||
taskCount: value.taskCount ?? null,
|
||||
taskRefMissingCount: value.taskRefMissingCount ?? null,
|
||||
selectedSourceId: value.selectedSourceId ?? null,
|
||||
selectedFileRef: value.selectedFileRef ?? null,
|
||||
selectedTaskRef: value.selectedTaskRef ?? null,
|
||||
selectedTaskStatus: value.selectedTaskStatus ?? null,
|
||||
taskStatusCounts: value.taskStatusCounts && typeof value.taskStatusCounts === "object" ? value.taskStatusCounts : {},
|
||||
launchButtonVisible: value.launchButtonVisible === true,
|
||||
launchButtonEnabled: value.launchButtonEnabled === true,
|
||||
launchButtonText: value.launchButtonText ?? null,
|
||||
blockerCount: value.blockerCount ?? 0,
|
||||
blockers: Array.isArray(value.blockers) ? value.blockers.slice(0, 6) : [],
|
||||
workbenchLinkCount: value.workbenchLinkCount ?? 0,
|
||||
valuesRedacted: true
|
||||
};
|
||||
}
|
||||
|
||||
function compactSessionRail(value) {
|
||||
if (!value || typeof value !== "object") return null;
|
||||
const items = Array.isArray(value.items) ? value.items.slice(0, 80).map((item) => ({
|
||||
@@ -917,6 +990,273 @@ function summarizeCommandFailures(control) {
|
||||
});
|
||||
}
|
||||
|
||||
function buildProjectManagementReport(samples, control, network, pagePerformance, config) {
|
||||
const enabled = config?.enabled === true;
|
||||
const targetPathSamples = (samples || []).filter((sample) => enabled && config.targetPaths.some((target) => String(sample?.path || "").startsWith(target)));
|
||||
const projectSamples = (samples || []).filter((sample) => sample?.projectManagement && typeof sample.projectManagement === "object");
|
||||
const latest = projectSamples[projectSamples.length - 1] || null;
|
||||
const latestProject = latest?.projectManagement || null;
|
||||
const pageKindCounts = countBy(projectSamples.map((sample) => sample.projectManagement?.pageKind).filter(Boolean));
|
||||
const latestTaskStatusCounts = latestProject?.taskStatusCounts && typeof latestProject.taskStatusCounts === "object" ? latestProject.taskStatusCounts : {};
|
||||
const commandRows = projectManagementCommandRows(control, config);
|
||||
const launchCommands = commandRows.filter((item) => item.type === "launchWorkbenchFromTask");
|
||||
const launchSuccess = launchCommands.filter((item) => item.phase === "completed" && Number(item.launchStatus ?? 0) >= 200 && Number(item.launchStatus ?? 0) < 300);
|
||||
const launchFailed = launchCommands.filter((item) => item.phase === "failed" || Number(item.launchStatus ?? 200) >= 400);
|
||||
const projectApiEvents = projectManagementNetworkRows(network, config);
|
||||
const projectApiResponses = projectApiEvents.filter((item) => item.type === "response");
|
||||
const projectApiFailures = projectApiResponses.filter((item) => Number(item.status ?? 0) >= 400);
|
||||
const projectApiFailedRequests = projectApiEvents.filter((item) => item.type === "requestfailed");
|
||||
const projectApiByPath = groupProjectApiEvents(projectApiEvents);
|
||||
const projectApiPerformance = projectManagementPerformanceRows(pagePerformance, config);
|
||||
const slowProjectApiPerformance = projectApiPerformance.filter((item) => Number(item.overBudgetCount ?? 0) > 0 || Number(item.p95Ms ?? 0) > Number(config?.slowApiBudgetMs ?? 0));
|
||||
const selectedTaskSamples = projectSamples.filter((sample) => sample.projectManagement?.selectedTaskRef?.hash);
|
||||
const launchEnabledSamples = projectSamples.filter((sample) => sample.projectManagement?.launchButtonEnabled === true);
|
||||
const launchVisibleSamples = projectSamples.filter((sample) => sample.projectManagement?.launchButtonVisible === true);
|
||||
const mdtodoSamples = projectSamples.filter((sample) => sample.projectManagement?.pageKind === "project-management-mdtodo");
|
||||
return {
|
||||
enabled,
|
||||
config: config || null,
|
||||
summary: {
|
||||
enabled,
|
||||
targetPathSampleCount: targetPathSamples.length,
|
||||
projectSampleCount: projectSamples.length,
|
||||
mdtodoSampleCount: mdtodoSamples.length,
|
||||
pageKindCounts,
|
||||
latestPageKind: latestProject?.pageKind ?? null,
|
||||
latestPath: latest?.path ?? null,
|
||||
latestSeq: latest?.seq ?? null,
|
||||
latestTs: latest?.ts ?? null,
|
||||
latestSourceCount: latestProject?.sourceCount ?? null,
|
||||
latestFileCount: latestProject?.fileCount ?? null,
|
||||
latestTaskCount: latestProject?.taskCount ?? null,
|
||||
maxSourceCount: maxNumber(projectSamples.map((sample) => sample.projectManagement?.sourceCount)),
|
||||
maxFileCount: maxNumber(projectSamples.map((sample) => sample.projectManagement?.fileCount)),
|
||||
maxTaskCount: maxNumber(projectSamples.map((sample) => sample.projectManagement?.taskCount)),
|
||||
taskRefMissingMax: maxNumber(projectSamples.map((sample) => sample.projectManagement?.taskRefMissingCount)),
|
||||
latestSelectedTaskRefHash: latestProject?.selectedTaskRef?.hash ?? null,
|
||||
latestSelectedTaskRefPreview: latestProject?.selectedTaskRef?.preview ?? null,
|
||||
latestSelectedTaskStatus: latestProject?.selectedTaskStatus ?? null,
|
||||
latestTaskStatusCounts,
|
||||
launchButtonVisibleSamples: launchVisibleSamples.length,
|
||||
launchButtonEnabledSamples: launchEnabledSamples.length,
|
||||
launchButtonDisabledSamples: Math.max(0, launchVisibleSamples.length - launchEnabledSamples.length),
|
||||
latestWorkbenchLinkCount: latestProject?.workbenchLinkCount ?? null,
|
||||
maxWorkbenchLinkCount: maxNumber(projectSamples.map((sample) => sample.projectManagement?.workbenchLinkCount)),
|
||||
maxBlockerCount: maxNumber(projectSamples.map((sample) => sample.projectManagement?.blockerCount)),
|
||||
selectedTaskSampleCount: selectedTaskSamples.length,
|
||||
projectCommandCount: commandRows.length,
|
||||
launchCommandCount: launchCommands.length,
|
||||
launchSuccessCount: launchSuccess.length,
|
||||
launchFailureCount: launchFailed.length,
|
||||
launchWithOtelTraceHeaderCount: launchSuccess.filter((item) => item.otelTraceId).length,
|
||||
projectApiEventCount: projectApiEvents.length,
|
||||
projectApiResponseCount: projectApiResponses.length,
|
||||
projectApiFailureCount: projectApiFailures.length,
|
||||
projectApiRequestFailedCount: projectApiFailedRequests.length,
|
||||
projectApiSlowPathCount: slowProjectApiPerformance.length,
|
||||
slowApiBudgetMs: config?.slowApiBudgetMs ?? null,
|
||||
valuesRedacted: true
|
||||
},
|
||||
latest: latestProject,
|
||||
samples: projectSamples.slice(-80).map((sample) => ({
|
||||
seq: sample.seq ?? null,
|
||||
ts: sample.ts ?? null,
|
||||
pageRole: sample.pageRole ?? null,
|
||||
path: sample.path ?? null,
|
||||
pageKind: sample.projectManagement?.pageKind ?? null,
|
||||
sourceCount: sample.projectManagement?.sourceCount ?? null,
|
||||
fileCount: sample.projectManagement?.fileCount ?? null,
|
||||
taskCount: sample.projectManagement?.taskCount ?? null,
|
||||
taskRefMissingCount: sample.projectManagement?.taskRefMissingCount ?? null,
|
||||
selectedTaskRefHash: sample.projectManagement?.selectedTaskRef?.hash ?? null,
|
||||
selectedTaskStatus: sample.projectManagement?.selectedTaskStatus ?? null,
|
||||
launchButtonVisible: sample.projectManagement?.launchButtonVisible === true,
|
||||
launchButtonEnabled: sample.projectManagement?.launchButtonEnabled === true,
|
||||
blockerCount: sample.projectManagement?.blockerCount ?? 0,
|
||||
workbenchLinkCount: sample.projectManagement?.workbenchLinkCount ?? 0,
|
||||
valuesRedacted: true
|
||||
})),
|
||||
targetPathWithoutProjectSummary: targetPathSamples.filter((sample) => !sample.projectManagement).slice(0, 20).map(ref),
|
||||
commands: commandRows,
|
||||
launchCommands,
|
||||
projectApiByPath,
|
||||
projectApiFailures: projectApiFailures.slice(0, 40),
|
||||
projectApiRequestFailed: projectApiFailedRequests.slice(0, 40),
|
||||
projectApiPerformance,
|
||||
slowProjectApiPerformance,
|
||||
valuesRedacted: true
|
||||
};
|
||||
}
|
||||
|
||||
function compactProjectManagementForOutput(report) {
|
||||
if (!report || typeof report !== "object") return null;
|
||||
const compactCommand = (item) => ({
|
||||
ts: item?.ts ?? null,
|
||||
phase: item?.phase ?? null,
|
||||
type: item?.type ?? null,
|
||||
commandId: item?.commandId ?? null,
|
||||
afterPath: item?.afterPath ?? null,
|
||||
launchStatus: item?.launchStatus ?? null,
|
||||
sessionId: item?.sessionId ?? null,
|
||||
workbenchUrl: item?.workbenchUrl ?? null,
|
||||
otelTraceId: item?.otelTraceId ?? null,
|
||||
contractVersion: item?.contractVersion ?? null,
|
||||
selectedTaskRefHash: item?.selectedTaskRefHash ?? null,
|
||||
errorMessageHash: item?.errorMessageHash ?? null,
|
||||
message: item?.message ? limitText(item.message, 180) : null,
|
||||
valuesRedacted: true
|
||||
});
|
||||
const compactApiGroup = (item) => ({
|
||||
method: item?.method ?? null,
|
||||
path: item?.path ?? item?.urlPath ?? null,
|
||||
status: item?.status ?? null,
|
||||
type: item?.type ?? null,
|
||||
count: item?.count ?? item?.sampleCount ?? null,
|
||||
firstAt: item?.firstAt ?? null,
|
||||
lastAt: item?.lastAt ?? null,
|
||||
failureKinds: Array.isArray(item?.failureKinds) ? item.failureKinds.slice(0, 4) : [],
|
||||
valuesRedacted: true
|
||||
});
|
||||
const compactSlowSample = (item) => ({
|
||||
ts: item?.ts ?? null,
|
||||
seq: item?.seq ?? null,
|
||||
path: item?.path ?? item?.rawPath ?? null,
|
||||
durationMs: item?.durationMs ?? null,
|
||||
requestToResponseStartMs: item?.requestToResponseStartMs ?? item?.streamOpenMs ?? null,
|
||||
responseTransferMs: item?.responseTransferMs ?? null,
|
||||
timingStatus: item?.timingStatus ?? null,
|
||||
initiatorType: item?.initiatorType ?? null,
|
||||
nextHopProtocol: item?.nextHopProtocol ?? null,
|
||||
serverTimingNames: Array.isArray(item?.serverTimingNames) ? item.serverTimingNames.slice(0, 4) : [],
|
||||
otelTraceId: item?.otelTraceId ?? null,
|
||||
valuesRedacted: true
|
||||
});
|
||||
const compactPerformance = (item) => ({
|
||||
path: item?.path ?? item?.route ?? null,
|
||||
sampleCount: item?.sampleCount ?? null,
|
||||
p95Ms: item?.p95Ms ?? item?.p95 ?? null,
|
||||
maxMs: item?.maxMs ?? item?.max ?? null,
|
||||
budgetMs: item?.projectSlowBudgetMs ?? item?.budgetMs ?? report.summary?.slowApiBudgetMs ?? null,
|
||||
overBudgetCount: item?.overBudgetCount ?? item?.overFiveSecondCount ?? null,
|
||||
slowSamples: Array.isArray(item?.slowSamples) ? item.slowSamples.slice(0, 3).map(compactSlowSample) : [],
|
||||
valuesRedacted: true
|
||||
});
|
||||
return {
|
||||
summary: report.summary ?? null,
|
||||
samples: Array.isArray(report.samples) ? report.samples.slice(-8) : [],
|
||||
commands: Array.isArray(report.commands) ? report.commands.slice(-8).map(compactCommand) : [],
|
||||
launchCommands: Array.isArray(report.launchCommands) ? report.launchCommands.slice(-8).map(compactCommand) : [],
|
||||
projectApiByPath: Array.isArray(report.projectApiByPath) ? report.projectApiByPath.slice(0, 8).map(compactApiGroup) : [],
|
||||
projectApiPerformance: Array.isArray(report.projectApiPerformance) ? report.projectApiPerformance.slice(0, 8).map(compactPerformance) : [],
|
||||
slowProjectApiPerformance: Array.isArray(report.slowProjectApiPerformance) ? report.slowProjectApiPerformance.slice(0, 8).map(compactPerformance) : [],
|
||||
valuesRedacted: true
|
||||
};
|
||||
}
|
||||
|
||||
function projectManagementCommandRows(control, config) {
|
||||
const allowed = new Set(config?.commandAllowlist || []);
|
||||
return (control || [])
|
||||
.filter((item) => allowed.has(item?.type) || String(item?.type || "").startsWith("selectMdtodo") || item?.type === "selectProjectSource" || item?.type === "launchWorkbenchFromTask")
|
||||
.filter((item) => item.phase === "completed" || item.phase === "failed")
|
||||
.map((item) => {
|
||||
const detail = item.detail && typeof item.detail === "object" ? item.detail : {};
|
||||
const error = detail.error && typeof detail.error === "object" ? detail.error : {};
|
||||
return {
|
||||
ts: item.ts ?? null,
|
||||
phase: item.phase ?? null,
|
||||
type: item.type ?? null,
|
||||
commandId: item.commandId ?? null,
|
||||
afterPath: urlPath(item.afterUrl),
|
||||
launchStatus: detail.launchStatus ?? error.details?.launchStatus ?? null,
|
||||
sessionId: detail.sessionId ?? error.details?.sessionId ?? null,
|
||||
workbenchUrl: detail.workbenchUrl ?? error.details?.workbenchUrl ?? null,
|
||||
otelTraceId: detail.otelTraceId ?? error.details?.otelTraceId ?? null,
|
||||
contractVersion: detail.contractVersion ?? error.details?.contractVersion ?? null,
|
||||
selectedTaskRefHash: detail.selectedTask?.hash ?? detail.projectBeforeClick?.selectedTaskRef?.hash ?? null,
|
||||
errorName: error.name ?? null,
|
||||
errorMessageHash: error.message ? sha256(error.message) : null,
|
||||
message: error.message ? limitText(error.message, 180) : null,
|
||||
valuesRedacted: true
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function projectManagementNetworkRows(network, config) {
|
||||
const prefixes = config?.naturalApiPathPrefixes || [];
|
||||
return (network || [])
|
||||
.filter((item) => item?.observerInitiated !== true)
|
||||
.map((item) => ({
|
||||
ts: item.ts ?? null,
|
||||
type: item.type ?? null,
|
||||
method: String(item.method || "GET").toUpperCase(),
|
||||
status: Number.isFinite(Number(item.status)) ? Number(item.status) : null,
|
||||
path: urlPath(item.url),
|
||||
failureKind: item.failure ? limitText(item.failure, 120) : null,
|
||||
valuesRedacted: true
|
||||
}))
|
||||
.filter((item) => prefixes.some((prefix) => String(item.path || "").startsWith(prefix)));
|
||||
}
|
||||
|
||||
function groupProjectApiEvents(events) {
|
||||
const groups = new Map();
|
||||
for (const item of events || []) {
|
||||
const key = [item.method, item.path, item.status ?? "-", item.type].join(" ");
|
||||
const existing = groups.get(key) || { method: item.method, path: item.path, status: item.status, type: item.type, count: 0, firstAt: item.ts, lastAt: item.ts, failureKinds: [], valuesRedacted: true };
|
||||
existing.count += 1;
|
||||
existing.lastAt = item.ts;
|
||||
if (item.failureKind && !existing.failureKinds.includes(item.failureKind)) existing.failureKinds.push(item.failureKind);
|
||||
groups.set(key, existing);
|
||||
}
|
||||
return Array.from(groups.values()).sort((a, b) => b.count - a.count || String(a.path).localeCompare(String(b.path)));
|
||||
}
|
||||
|
||||
function projectManagementPerformanceRows(pagePerformance, config) {
|
||||
const prefixes = config?.naturalApiPathPrefixes || [];
|
||||
const rows = Array.isArray(pagePerformance?.sameOriginApiByPath) ? pagePerformance.sameOriginApiByPath : [];
|
||||
return rows
|
||||
.filter((item) => prefixes.some((prefix) => String(item?.path || "").startsWith(prefix)))
|
||||
.map((item) => ({ ...item, projectSlowBudgetMs: config?.slowApiBudgetMs ?? null }));
|
||||
}
|
||||
|
||||
function buildProjectManagementFindings(report) {
|
||||
if (!report?.enabled) return [];
|
||||
const findings = [];
|
||||
const summary = report.summary || {};
|
||||
if (Number(summary.targetPathSampleCount ?? 0) > 0 && Number(summary.projectSampleCount ?? 0) === 0) {
|
||||
findings.push({ id: "project-management-route-not-ready", severity: "red", summary: "project management target path was sampled but no project-management DOM summary was detected", count: summary.targetPathSampleCount, samples: report.targetPathWithoutProjectSummary, valuesRedacted: true });
|
||||
}
|
||||
if (Number(summary.taskRefMissingMax ?? 0) > 0) {
|
||||
findings.push({ id: "mdtodo-taskref-missing", severity: "red", summary: "mdtodo task rows were visible without stable data-task-ref; Workbench launch must bind by opaque public task id", count: summary.taskRefMissingMax, samples: report.samples.filter((item) => Number(item.taskRefMissingCount ?? 0) > 0).slice(0, 20), valuesRedacted: true });
|
||||
}
|
||||
if (Number(summary.mdtodoSampleCount ?? 0) > 0 && Number(summary.latestTaskCount ?? 0) > 0 && Number(summary.launchButtonEnabledSamples ?? 0) === 0) {
|
||||
findings.push({ id: "workbench-launch-button-unavailable", severity: "red", summary: "mdtodo tasks were sampled but the Workbench launch button was never enabled", count: summary.mdtodoSampleCount, latest: report.latest, valuesRedacted: true });
|
||||
}
|
||||
if (Number(summary.projectApiFailureCount ?? 0) > 0 || Number(summary.projectApiRequestFailedCount ?? 0) > 0) {
|
||||
findings.push({ id: "project-management-api-failed", severity: "amber", summary: "natural project-management or Workbench launch API requests failed during observation", count: Number(summary.projectApiFailureCount ?? 0) + Number(summary.projectApiRequestFailedCount ?? 0), groups: report.projectApiByPath.slice(0, 12), valuesRedacted: true });
|
||||
}
|
||||
if (Number(summary.projectApiSlowPathCount ?? 0) > 0) {
|
||||
findings.push({ id: "project-management-api-slow", severity: "red", summary: "project-management API resource timing exceeded YAML projectManagement.slowApiBudgetMs", count: summary.projectApiSlowPathCount, budgetMs: summary.slowApiBudgetMs, groups: report.slowProjectApiPerformance.slice(0, 12), valuesRedacted: true });
|
||||
}
|
||||
if (Number(summary.launchFailureCount ?? 0) > 0) {
|
||||
findings.push({ id: "mdtodo-workbench-launch-failed", severity: "red", summary: "launchWorkbenchFromTask command failed or returned an HTTP error", count: summary.launchFailureCount, commands: report.launchCommands.filter((item) => item.phase === "failed" || Number(item.launchStatus ?? 200) >= 400).slice(0, 12), valuesRedacted: true });
|
||||
}
|
||||
if (Number(summary.launchSuccessCount ?? 0) > 0 && Number(summary.launchWithOtelTraceHeaderCount ?? 0) === 0) {
|
||||
findings.push({ id: "mdtodo-workbench-launch-otel-trace-missing", severity: "amber", summary: "Workbench launch succeeded but no x-hwlab-otel-trace-id header was captured for Tempo drill-down", count: summary.launchSuccessCount, commands: report.launchCommands.slice(0, 12), valuesRedacted: true });
|
||||
}
|
||||
return findings;
|
||||
}
|
||||
|
||||
function countBy(values) {
|
||||
const out = {};
|
||||
for (const value of values || []) out[value] = (out[value] || 0) + 1;
|
||||
return out;
|
||||
}
|
||||
|
||||
function maxNumber(values) {
|
||||
const numeric = (values || []).map((value) => Number(value)).filter(Number.isFinite);
|
||||
return numeric.length > 0 ? Math.max(...numeric) : 0;
|
||||
}
|
||||
|
||||
function buildFindings(samples, control, network, errors, sampleMetrics, promptNetwork, runtimeAlerts, pagePerformance, pageProvenance, commandFailures = []) {
|
||||
const findings = [];
|
||||
if (commandFailures.length > 0) findings.push({ id: "observer-command-failed", severity: "red", summary: "observer control commands failed; analyze must surface command failure instead of hiding it in command artifacts", count: commandFailures.length, commands: commandFailures.slice(0, 20) });
|
||||
@@ -2140,6 +2480,7 @@ function prioritizeFindings(findings) {
|
||||
};
|
||||
const kindRank = (item) => {
|
||||
const id = String(item?.id ?? item?.kind ?? item?.code ?? "");
|
||||
if (id.startsWith("project-management-") || id.startsWith("mdtodo-") || id === "workbench-launch-button-unavailable") return 0;
|
||||
if (id === "page-performance-slow-same-origin-api") return 0;
|
||||
if (id === "session-rail-title-fallback-majority") return 0.5;
|
||||
if (id.startsWith("code-agent-card-")) return 0.8;
|
||||
|
||||
Reference in New Issue
Block a user