Merge pull request #1084 from pikasTech/fix/1078-api-dom-lag-evidence
fix(web-probe): report API-DOM lag candidates
This commit is contained in:
@@ -54,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 apiDomLag = buildApiDomLagReport(samples, network);
|
||||
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 : [];
|
||||
@@ -97,7 +98,7 @@ const runnerErrors = errors.slice(-8).map((item) => {
|
||||
});
|
||||
const commandFailures = summarizeCommandFailures(control);
|
||||
const toolFindings = buildToolFindings({ manifest, heartbeat, commandState });
|
||||
const findings = [...toolFindings, ...buildProjectManagementFindings(projectManagement), ...buildFindings(samples, control, network, errors, sampleMetrics, promptNetwork, runtimeAlerts, pagePerformance, pageProvenance, commandFailures, manifest)];
|
||||
const findings = [...toolFindings, ...buildProjectManagementFindings(projectManagement), ...buildFindings(samples, control, network, errors, sampleMetrics, promptNetwork, runtimeAlerts, pagePerformance, pageProvenance, commandFailures, manifest, apiDomLag)];
|
||||
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 }));
|
||||
@@ -119,6 +120,7 @@ const report = {
|
||||
projectManagement,
|
||||
promptNetwork,
|
||||
runtimeAlerts,
|
||||
apiDomLag,
|
||||
runnerErrors,
|
||||
commandFailures,
|
||||
commandState,
|
||||
@@ -147,6 +149,7 @@ console.log(JSON.stringify({
|
||||
pagePerformance: pagePerformance.summary,
|
||||
projectManagement: projectManagement.summary,
|
||||
runtimeAlerts: runtimeAlerts.summary,
|
||||
apiDomLag: apiDomLag.summary,
|
||||
findingCount: findings.length,
|
||||
redFindingCount: findings.filter((item) => item.severity === "red").length,
|
||||
redFindings: prioritizeFindings(findings.filter((item) => item.severity === "red")).slice(0, 12).map((item) => ({ kind: item.id ?? item.kind ?? item.code, severity: item.severity, count: item.count ?? item.sampleCount ?? null, summary: String(item.summary ?? item.message ?? "").slice(0, 180) })),
|
||||
@@ -166,6 +169,7 @@ console.log(JSON.stringify({
|
||||
projectManagement: compactProjectManagementForOutput(projectManagement),
|
||||
promptNetwork: recentWindow.promptNetwork.summary,
|
||||
runtimeAlerts: recentWindow.runtimeAlerts.summary,
|
||||
apiDomLag: compactApiDomLagForOutput(apiDomLag),
|
||||
runnerErrors,
|
||||
commandFailures: commandFailures.slice(-8),
|
||||
commandState,
|
||||
@@ -1506,8 +1510,320 @@ function timestampMs(value) {
|
||||
return Number.isFinite(parsed) ? parsed : NaN;
|
||||
}
|
||||
|
||||
function buildFindings(samples, control, network, errors, sampleMetrics, promptNetwork, runtimeAlerts, pagePerformance, pageProvenance, commandFailures = [], manifest = {}) {
|
||||
function buildApiDomLagReport(samples, network) {
|
||||
const windowMs = 30_000;
|
||||
const budgetMs = Number.isFinite(Number(alertThresholds.sameOriginApiSlowMs)) ? Number(alertThresholds.sameOriginApiSlowMs) : 10_000;
|
||||
const sampleRows = (Array.isArray(samples) ? samples : [])
|
||||
.map((sample) => {
|
||||
const tsMs = timestampMs(sample?.ts);
|
||||
return {
|
||||
sample,
|
||||
tsMs,
|
||||
pageKey: samplePageKey(sample),
|
||||
digest: digestSample(sample),
|
||||
sessionIds: new Set([sample?.routeSessionId, sample?.activeSessionId].filter(Boolean).map(String)),
|
||||
traceIds: sampleTraceIds(sample)
|
||||
};
|
||||
})
|
||||
.filter((item) => Number.isFinite(item.tsMs))
|
||||
.sort((a, b) => a.tsMs - b.tsMs);
|
||||
const samplesByPage = new Map();
|
||||
for (const row of sampleRows) {
|
||||
const rows = samplesByPage.get(row.pageKey) || [];
|
||||
rows.push(row);
|
||||
samplesByPage.set(row.pageKey, rows);
|
||||
}
|
||||
const naturalApiResponses = (Array.isArray(network) ? network : [])
|
||||
.filter((item) => item?.observerInitiated !== true && item?.type === "response" && isApiLikePath(urlPath(item?.url)));
|
||||
const telemetryExcluded = [];
|
||||
const nonStateRelevant = [];
|
||||
const stateRelevantResponses = [];
|
||||
for (const item of naturalApiResponses) {
|
||||
const event = compactApiDomLagResponseEvent(item);
|
||||
if (!Number.isFinite(event.tsMs)) {
|
||||
nonStateRelevant.push(event);
|
||||
continue;
|
||||
}
|
||||
if (isApiDomLagTelemetryPath(event.path)) telemetryExcluded.push(event);
|
||||
else if (!isApiDomLagStateRelevantPath(event.path)) nonStateRelevant.push(event);
|
||||
else stateRelevantResponses.push(event);
|
||||
}
|
||||
const candidates = [];
|
||||
for (const event of stateRelevantResponses) {
|
||||
const pageSamples = samplesByPage.get(event.pageKey) || [];
|
||||
const before = lastSampleAtOrBefore(pageSamples, event.tsMs, event);
|
||||
const firstAfter = firstSampleAfter(pageSamples, event.tsMs, event.tsMs + windowMs, event);
|
||||
const baselineDigest = before?.digest ?? null;
|
||||
const change = firstSampleAfter(pageSamples, event.tsMs, event.tsMs + windowMs, event, (row) => !baselineDigest || row.digest !== baselineDigest);
|
||||
candidates.push({
|
||||
...event,
|
||||
windowMs,
|
||||
budgetMs,
|
||||
firstSampleDeltaMs: firstAfter ? Math.max(0, Math.round(firstAfter.tsMs - event.tsMs)) : null,
|
||||
domChangeDeltaMs: change ? Math.max(0, Math.round(change.tsMs - event.tsMs)) : null,
|
||||
overBudget: change ? (change.tsMs - event.tsMs) > budgetMs : false,
|
||||
domChanged: Boolean(change),
|
||||
noDomChangeWithinWindow: !change,
|
||||
beforeSample: compactApiDomLagSample(before),
|
||||
firstAfterSample: compactApiDomLagSample(firstAfter),
|
||||
changeSample: compactApiDomLagSample(change),
|
||||
confidence: apiDomLagConfidence(event.path),
|
||||
valuesRedacted: true
|
||||
});
|
||||
}
|
||||
const changedDeltas = candidates.map((item) => nullableNumber(item.domChangeDeltaMs)).filter(Number.isFinite).sort((a, b) => a - b);
|
||||
const groups = groupApiDomLagCandidates(candidates);
|
||||
const overBudget = candidates.filter((item) => item.overBudget === true);
|
||||
return {
|
||||
summary: {
|
||||
windowMs,
|
||||
budgetMs,
|
||||
naturalApiResponseCount: naturalApiResponses.length,
|
||||
telemetryExcludedCount: telemetryExcluded.length,
|
||||
nonStateRelevantResponseCount: nonStateRelevant.length,
|
||||
stateRelevantResponseCount: stateRelevantResponses.length,
|
||||
candidateCount: candidates.length,
|
||||
domChangedCount: changedDeltas.length,
|
||||
noDomChangeWithinWindowCount: candidates.filter((item) => item.noDomChangeWithinWindow === true).length,
|
||||
lowConfidenceStreamOpenCount: candidates.filter((item) => item.confidence === "low-stream-open-only").length,
|
||||
overBudgetCount: overBudget.length,
|
||||
p50DomChangeDeltaMs: percentile(changedDeltas, 50),
|
||||
p95DomChangeDeltaMs: percentile(changedDeltas, 95),
|
||||
maxDomChangeDeltaMs: changedDeltas.length > 0 ? Math.max(...changedDeltas) : null,
|
||||
groupCount: groups.length,
|
||||
valuesRedacted: true
|
||||
},
|
||||
groups,
|
||||
worstCandidates: candidates
|
||||
.filter((item) => Number.isFinite(nullableNumber(item.domChangeDeltaMs)))
|
||||
.sort((a, b) => nullableNumber(b.domChangeDeltaMs) - nullableNumber(a.domChangeDeltaMs))
|
||||
.slice(0, 20),
|
||||
recentCandidates: candidates.slice(-40),
|
||||
telemetryExcluded: telemetryExcluded.slice(0, 20),
|
||||
nonStateRelevant: nonStateRelevant.slice(0, 20),
|
||||
valuesRedacted: true
|
||||
};
|
||||
}
|
||||
|
||||
function compactApiDomLagResponseEvent(item) {
|
||||
const parsed = parseApiDomLagUrl(item?.url);
|
||||
const tsMs = timestampMs(item?.ts);
|
||||
return {
|
||||
ts: item?.ts ?? null,
|
||||
tsMs,
|
||||
pageRole: item?.pageRole ?? null,
|
||||
pageId: item?.pageId ?? null,
|
||||
pageKey: String(item?.pageRole || "control") + ":" + String(item?.pageId || "default"),
|
||||
commandId: item?.commandId ?? null,
|
||||
method: String(item?.method || "GET").toUpperCase(),
|
||||
status: Number.isFinite(Number(item?.status)) ? Number(item.status) : null,
|
||||
path: parsed.path,
|
||||
rawPath: parsed.rawPath,
|
||||
queryKeys: parsed.queryKeys,
|
||||
sessionId: parsed.sessionId,
|
||||
traceId: parsed.traceId,
|
||||
urlHash: item?.url ? sha256(item.url) : null,
|
||||
routeKind: apiDomLagRouteKind(parsed.path),
|
||||
valuesRedacted: true
|
||||
};
|
||||
}
|
||||
|
||||
function parseApiDomLagUrl(value) {
|
||||
try {
|
||||
const parsed = new URL(String(value || "http://invalid.local/"));
|
||||
const rawPath = parsed.pathname || "-";
|
||||
const queryKeys = Array.from(parsed.searchParams.keys()).sort().slice(0, 12);
|
||||
const sessionId = parsed.searchParams.get("sessionId") || parsed.searchParams.get("includeSessionId") || firstIdInText(parsed.pathname + " " + parsed.search, /\bses_[A-Za-z0-9_-]+\b/u);
|
||||
const traceId = parsed.searchParams.get("traceId") || firstIdInText(parsed.pathname + " " + parsed.search, /\btrc_[A-Za-z0-9_-]+\b/u);
|
||||
return {
|
||||
rawPath,
|
||||
path: normalizeApiPath(rawPath),
|
||||
queryKeys,
|
||||
sessionId,
|
||||
traceId
|
||||
};
|
||||
} catch {
|
||||
const rawPath = urlPath(value);
|
||||
return {
|
||||
rawPath,
|
||||
path: normalizeApiPath(rawPath),
|
||||
queryKeys: [],
|
||||
sessionId: firstIdInText(String(value || ""), /\bses_[A-Za-z0-9_-]+\b/u),
|
||||
traceId: firstIdInText(String(value || ""), /\btrc_[A-Za-z0-9_-]+\b/u)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function firstIdInText(text, pattern) {
|
||||
const match = String(text || "").match(pattern);
|
||||
return match ? match[0] : null;
|
||||
}
|
||||
|
||||
function nullableNumber(value) {
|
||||
if (value === null || value === undefined || value === "") return NaN;
|
||||
const numeric = Number(value);
|
||||
return Number.isFinite(numeric) ? numeric : NaN;
|
||||
}
|
||||
|
||||
function isApiDomLagTelemetryPath(path) {
|
||||
const value = String(path || "");
|
||||
return value === "/v1/web-performance" || value === "/v1/health" || value === "/health";
|
||||
}
|
||||
|
||||
function isApiDomLagStateRelevantPath(path) {
|
||||
const value = String(path || "");
|
||||
return value.startsWith("/auth/") || value.startsWith("/v1/workbench/") || value === "/v1/agent/chat" || value === "/v1/agent/chat/steer";
|
||||
}
|
||||
|
||||
function apiDomLagRouteKind(path) {
|
||||
const value = String(path || "");
|
||||
if (value === "/v1/workbench/events") return "workbench-events-stream";
|
||||
if (value.startsWith("/v1/workbench/sessions")) return "workbench-sessions";
|
||||
if (value.startsWith("/v1/workbench/traces")) return "workbench-traces";
|
||||
if (value.startsWith("/v1/workbench/turns")) return "workbench-turns";
|
||||
if (value === "/v1/agent/chat" || value === "/v1/agent/chat/steer") return "agent-chat-submit";
|
||||
if (value.startsWith("/auth/")) return "auth";
|
||||
return "state-api";
|
||||
}
|
||||
|
||||
function apiDomLagConfidence(path) {
|
||||
return String(path || "") === "/v1/workbench/events" ? "low-stream-open-only" : "medium-response-to-dom";
|
||||
}
|
||||
|
||||
function sampleTraceIds(sample) {
|
||||
const ids = new Set();
|
||||
for (const group of [sample?.messages, sample?.traceRows, sample?.turns, sample?.diagnostics]) {
|
||||
if (!Array.isArray(group)) continue;
|
||||
for (const item of group) if (item?.traceId) ids.add(String(item.traceId));
|
||||
}
|
||||
return ids;
|
||||
}
|
||||
|
||||
function lastSampleAtOrBefore(rows, tsMs, event) {
|
||||
let result = null;
|
||||
for (const row of rows) {
|
||||
if (row.tsMs > tsMs) break;
|
||||
if (apiDomLagSampleMatchesEvent(row, event)) result = row;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function firstSampleAfter(rows, startMs, endMs, event, predicate = null) {
|
||||
for (const row of rows) {
|
||||
if (row.tsMs < startMs) continue;
|
||||
if (row.tsMs > endMs) break;
|
||||
if (!apiDomLagSampleMatchesEvent(row, event)) continue;
|
||||
if (typeof predicate === "function" && !predicate(row)) continue;
|
||||
return row;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function apiDomLagSampleMatchesEvent(row, event) {
|
||||
if (!row || !event) return false;
|
||||
if (event.sessionId && !row.sessionIds.has(String(event.sessionId))) return false;
|
||||
if (event.traceId && row.traceIds.size > 0 && !row.traceIds.has(String(event.traceId))) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
function compactApiDomLagSample(row) {
|
||||
if (!row) return null;
|
||||
const sample = row.sample || {};
|
||||
return {
|
||||
seq: sample.seq ?? null,
|
||||
ts: sample.ts ?? null,
|
||||
pageRole: sample.pageRole ?? null,
|
||||
pageId: sample.pageId ?? null,
|
||||
routeSessionId: sample.routeSessionId ?? null,
|
||||
activeSessionId: sample.activeSessionId ?? null,
|
||||
messageCount: Array.isArray(sample.messages) ? sample.messages.length : null,
|
||||
traceRowCount: Array.isArray(sample.traceRows) ? sample.traceRows.length : null,
|
||||
diagnosticCount: Array.isArray(sample.diagnostics) ? sample.diagnostics.length : null,
|
||||
valuesRedacted: true
|
||||
};
|
||||
}
|
||||
|
||||
function groupApiDomLagCandidates(candidates) {
|
||||
const groups = new Map();
|
||||
for (const item of candidates || []) {
|
||||
const key = [item.method || "-", item.path || "-", item.status ?? "-", item.confidence || "-"].join(" ");
|
||||
const group = groups.get(key) || {
|
||||
method: item.method ?? null,
|
||||
path: item.path ?? "-",
|
||||
routeKind: item.routeKind ?? null,
|
||||
status: item.status ?? null,
|
||||
confidence: item.confidence ?? null,
|
||||
count: 0,
|
||||
domChangedCount: 0,
|
||||
noDomChangeWithinWindowCount: 0,
|
||||
overBudgetCount: 0,
|
||||
firstAt: item.ts ?? null,
|
||||
lastAt: item.ts ?? null,
|
||||
deltas: [],
|
||||
examples: []
|
||||
};
|
||||
group.count += 1;
|
||||
group.firstAt = minIso(group.firstAt, item.ts ?? null);
|
||||
group.lastAt = maxIso(group.lastAt, item.ts ?? null);
|
||||
if (item.domChanged === true && Number.isFinite(Number(item.domChangeDeltaMs))) {
|
||||
group.domChangedCount += 1;
|
||||
group.deltas.push(Number(item.domChangeDeltaMs));
|
||||
}
|
||||
if (item.noDomChangeWithinWindow === true) group.noDomChangeWithinWindowCount += 1;
|
||||
if (item.overBudget === true) group.overBudgetCount += 1;
|
||||
if (group.examples.length < 6) {
|
||||
group.examples.push({
|
||||
ts: item.ts ?? null,
|
||||
sessionId: item.sessionId ?? null,
|
||||
traceId: item.traceId ?? null,
|
||||
domChangeDeltaMs: item.domChangeDeltaMs ?? null,
|
||||
firstSampleDeltaMs: item.firstSampleDeltaMs ?? null,
|
||||
changeSeq: item.changeSample?.seq ?? null,
|
||||
beforeSeq: item.beforeSample?.seq ?? null,
|
||||
valuesRedacted: true
|
||||
});
|
||||
}
|
||||
groups.set(key, group);
|
||||
}
|
||||
return Array.from(groups.values())
|
||||
.map((item) => {
|
||||
const deltas = item.deltas.slice().sort((a, b) => a - b);
|
||||
return {
|
||||
method: item.method,
|
||||
path: item.path,
|
||||
routeKind: item.routeKind,
|
||||
status: item.status,
|
||||
confidence: item.confidence,
|
||||
count: item.count,
|
||||
domChangedCount: item.domChangedCount,
|
||||
noDomChangeWithinWindowCount: item.noDomChangeWithinWindowCount,
|
||||
overBudgetCount: item.overBudgetCount,
|
||||
p50DomChangeDeltaMs: percentile(deltas, 50),
|
||||
p95DomChangeDeltaMs: percentile(deltas, 95),
|
||||
maxDomChangeDeltaMs: deltas.length > 0 ? Math.max(...deltas) : null,
|
||||
firstAt: item.firstAt,
|
||||
lastAt: item.lastAt,
|
||||
examples: item.examples,
|
||||
valuesRedacted: true
|
||||
};
|
||||
})
|
||||
.sort((a, b) => Number(b.maxDomChangeDeltaMs ?? -1) - Number(a.maxDomChangeDeltaMs ?? -1) || b.count - a.count || String(a.path).localeCompare(String(b.path)));
|
||||
}
|
||||
|
||||
function compactApiDomLagForOutput(report) {
|
||||
if (!report || typeof report !== "object") return null;
|
||||
return {
|
||||
summary: report.summary ?? null,
|
||||
groups: Array.isArray(report.groups) ? report.groups.slice(0, 8) : [],
|
||||
worstCandidates: Array.isArray(report.worstCandidates) ? report.worstCandidates.slice(0, 8) : [],
|
||||
recentCandidates: Array.isArray(report.recentCandidates) ? report.recentCandidates.slice(-8) : [],
|
||||
valuesRedacted: true
|
||||
};
|
||||
}
|
||||
|
||||
function buildFindings(samples, control, network, errors, sampleMetrics, promptNetwork, runtimeAlerts, pagePerformance, pageProvenance, commandFailures = [], manifest = {}, apiDomLag = null) {
|
||||
const findings = [];
|
||||
const effectiveApiDomLag = apiDomLag || buildApiDomLagReport(samples, network);
|
||||
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) });
|
||||
findings.push(...buildSessionInvariantFindings(control, manifest));
|
||||
const commandTimes = control
|
||||
@@ -1674,7 +1990,21 @@ function buildFindings(samples, control, network, errors, sampleMetrics, promptN
|
||||
if (longLivedStreams.length > 0) findings.push({ id: "page-performance-long-lived-streams", severity: "info", summary: "same-origin long-lived streams are reported separately; lifetime is not treated as API load latency", count: longLivedStreams.length, groups: longLivedStreams.slice(0, 20) });
|
||||
if ((pageProvenance?.summary?.segmentCount ?? 0) > 1) findings.push({ id: "page-provenance-segments", severity: "info", summary: "observer crossed page asset provenance segments; interpret runtime findings by segment", segmentCount: pageProvenance.summary.segmentCount, segments: pageProvenance.segments.slice(0, 20) });
|
||||
const naturalApi = network.filter((item) => item.observerInitiated === false && item.type === "response" && /\/v1\/|\/auth\//u.test(String(item.url || "")));
|
||||
findings.push({ id: "natural-api-dom-lag-baseline", severity: "info", summary: "natural API responses and DOM samples are available for manual lag correlation", naturalApiResponses: naturalApi.length, sampleCount: samples.length });
|
||||
const apiDomLagSummary = effectiveApiDomLag?.summary || {};
|
||||
findings.push({ id: "natural-api-dom-lag-baseline", severity: "info", summary: "natural API responses and DOM samples are available for API-to-DOM lag correlation", naturalApiResponses: naturalApi.length, sampleCount: samples.length, apiDomLag: apiDomLagSummary });
|
||||
findings.push({
|
||||
id: "natural-api-dom-lag-candidates",
|
||||
severity: Number(apiDomLagSummary.overBudgetCount ?? 0) > 0 ? "amber" : "info",
|
||||
summary: "state-relevant natural API responses were correlated to the first subsequent DOM digest change; over-budget lag is a non-blocking investigation alert",
|
||||
count: apiDomLagSummary.candidateCount ?? 0,
|
||||
domChangedCount: apiDomLagSummary.domChangedCount ?? 0,
|
||||
noDomChangeWithinWindowCount: apiDomLagSummary.noDomChangeWithinWindowCount ?? 0,
|
||||
overBudgetCount: apiDomLagSummary.overBudgetCount ?? 0,
|
||||
budgetMs: apiDomLagSummary.budgetMs ?? null,
|
||||
p95DomChangeDeltaMs: apiDomLagSummary.p95DomChangeDeltaMs ?? null,
|
||||
maxDomChangeDeltaMs: apiDomLagSummary.maxDomChangeDeltaMs ?? null,
|
||||
groups: Array.isArray(effectiveApiDomLag?.groups) ? effectiveApiDomLag.groups.slice(0, 12) : [],
|
||||
});
|
||||
if (errors.length > 0) findings.push({ id: "browser-console-or-page-errors", severity: "amber", summary: "pageerror/runner errors were captured", count: errors.length, first: errors.slice(0, 5) });
|
||||
if (samples.length === 0) findings.push({ id: "no-samples", severity: "red", summary: "observer produced no samples" });
|
||||
return findings;
|
||||
@@ -1699,7 +2029,8 @@ function buildRecentAnalysisWindow({ samples, control, network, consoleEvents, e
|
||||
const pagePerformance = buildPagePerformanceReport(windowSamples, manifest);
|
||||
const promptNetwork = buildPromptNetworkReport(windowControl, windowNetwork);
|
||||
const runtimeAlerts = buildRuntimeAlerts(windowSamples, control, windowNetwork, windowConsole, windowErrors);
|
||||
const findings = buildFindings(windowSamples, control, windowNetwork, windowErrors, sampleMetrics, promptNetwork, runtimeAlerts, pagePerformance, pageProvenance);
|
||||
const apiDomLag = buildApiDomLagReport(windowSamples, windowNetwork);
|
||||
const findings = buildFindings(windowSamples, control, windowNetwork, windowErrors, sampleMetrics, promptNetwork, runtimeAlerts, pagePerformance, pageProvenance, [], {}, apiDomLag);
|
||||
return {
|
||||
summary: {
|
||||
name: "recent-5m",
|
||||
@@ -1718,6 +2049,7 @@ function buildRecentAnalysisWindow({ samples, control, network, consoleEvents, e
|
||||
pagePerformance,
|
||||
promptNetwork,
|
||||
runtimeAlerts,
|
||||
apiDomLag,
|
||||
findings,
|
||||
valuesRedacted: true
|
||||
};
|
||||
|
||||
@@ -2164,6 +2164,8 @@ function renderMarkdown(report) {
|
||||
const traceOrder = report.sampleMetrics?.traceOrder || {};
|
||||
const traceOrderSummary = traceOrder.summary || {};
|
||||
const alertSummary = report.runtimeAlerts?.summary || {};
|
||||
const apiDomLag = report.apiDomLag || {};
|
||||
const apiDomLagSummary = apiDomLag.summary || {};
|
||||
const projectManagement = report.projectManagement || {};
|
||||
const projectSummary = projectManagement.summary || {};
|
||||
const projectCommandLines = Array.isArray(projectManagement.commands) && projectManagement.commands.length > 0
|
||||
@@ -2254,6 +2256,12 @@ function renderMarkdown(report) {
|
||||
const streamPerformanceLines = streamPerformanceItems.length > 0
|
||||
? streamPerformanceItems.slice(0, 80).map((item) => "- " + item.path + " kind=" + (item.routeKind || "same-origin-api-stream") + " samples=" + item.sampleCount + " streamOpenP50=" + (item.streamOpenP50Ms ?? "-") + "ms streamOpenP75=" + (item.streamOpenP75Ms ?? "-") + "ms streamOpenP95=" + (item.streamOpenP95Ms ?? "-") + "ms streamOpenMax=" + (item.streamOpenMaxMs ?? "-") + "ms streamOpen>budget=" + (item.streamOpenOverBudgetCount ?? item.streamOpenOverFiveSecondCount ?? 0) + " streamOpenBudgetMs=" + (item.streamOpenBudgetMs ?? streamOpenBudgetMs) + " streamOpenLegacy>5s=" + (item.streamOpenOverFiveSecondCount ?? 0) + " streamLifetime>5s=" + (item.streamLifetimeOverFiveSecondCount ?? 0) + " lifetimeMax=" + (item.maxMs ?? "-") + "ms window=" + (item.firstAt || "-") + ".." + (item.lastAt || "-")).join("\n")
|
||||
: "- 无同源长连接 Resource Timing 样本。";
|
||||
const apiDomLagGroupLines = Array.isArray(apiDomLag.groups) && apiDomLag.groups.length > 0
|
||||
? apiDomLag.groups.slice(0, 80).map((item) => "- " + (item.method || "-") + " " + (item.path || "-") + " status=" + (item.status ?? "-") + " kind=" + (item.routeKind || "-") + " confidence=" + (item.confidence || "-") + " count=" + (item.count ?? 0) + " changed=" + (item.domChangedCount ?? 0) + " noChange=" + (item.noDomChangeWithinWindowCount ?? 0) + " p50=" + (item.p50DomChangeDeltaMs ?? "-") + "ms p95=" + (item.p95DomChangeDeltaMs ?? "-") + "ms max=" + (item.maxDomChangeDeltaMs ?? "-") + "ms overBudget=" + (item.overBudgetCount ?? 0) + " window=" + (item.firstAt || "-") + ".." + (item.lastAt || "-")).join("\n")
|
||||
: "- 无 API-DOM 候选分组。";
|
||||
const apiDomLagWorstLines = Array.isArray(apiDomLag.worstCandidates) && apiDomLag.worstCandidates.length > 0
|
||||
? apiDomLag.worstCandidates.slice(0, 40).map((item) => "- " + (item.ts || "-") + " " + (item.method || "-") + " " + (item.path || "-") + " status=" + (item.status ?? "-") + " confidence=" + (item.confidence || "-") + " domChange=" + (item.domChangeDeltaMs ?? "-") + "ms firstSample=" + (item.firstSampleDeltaMs ?? "-") + "ms session=" + (item.sessionId || "-") + " traceId=" + (item.traceId || "-") + " beforeSeq=" + (item.beforeSample?.seq ?? "-") + " changeSeq=" + (item.changeSample?.seq ?? "-")).join("\n")
|
||||
: "- 无 API-DOM digest 变化候选。";
|
||||
const metricLines = Array.isArray(report.sampleMetrics?.timeline) && report.sampleMetrics.timeline.length > 0
|
||||
? report.sampleMetrics.timeline.slice(0, 120).map((item) => "- #" + item.seq + " " + item.ts + " prompt=" + item.promptIndex + " loadingCount=" + (item.loadingCount ?? 0) + " loadingOwners=" + (item.loadingOwnerCount ?? 0) + " totalElapsedSeconds=" + (item.totalElapsedSeconds ?? "-") + " recentUpdateSeconds=" + (item.recentUpdateSeconds ?? "-") + " terminal=" + item.terminalSeen + " finalText=" + item.finalResultTextSeen + " diagnostic=" + item.diagnosticSeen).join("\n")
|
||||
: "- 无采样指标。";
|
||||
@@ -2403,6 +2411,20 @@ function renderMarkdown(report) {
|
||||
+ "### Page performance: long-lived streams\n\n"
|
||||
+ "- policy: SSE/long-lived stream lifetime is not ordinary API load latency; only stream open latency is compared with the YAML usability budget, while disconnects remain runtime alerts.\n\n"
|
||||
+ streamPerformanceLines + "\n\n"
|
||||
+ "### Natural API to DOM lag candidates\n\n"
|
||||
+ "- naturalApiResponseCount: " + (apiDomLagSummary.naturalApiResponseCount ?? 0) + "\n"
|
||||
+ "- stateRelevantResponseCount: " + (apiDomLagSummary.stateRelevantResponseCount ?? 0) + "\n"
|
||||
+ "- candidateCount: " + (apiDomLagSummary.candidateCount ?? 0) + "\n"
|
||||
+ "- domChangedCount: " + (apiDomLagSummary.domChangedCount ?? 0) + "\n"
|
||||
+ "- noDomChangeWithinWindowCount: " + (apiDomLagSummary.noDomChangeWithinWindowCount ?? 0) + "\n"
|
||||
+ "- budgetMs: " + (apiDomLagSummary.budgetMs ?? "-") + "\n"
|
||||
+ "- p95DomChangeDeltaMs: " + (apiDomLagSummary.p95DomChangeDeltaMs ?? "-") + "\n"
|
||||
+ "- maxDomChangeDeltaMs: " + (apiDomLagSummary.maxDomChangeDeltaMs ?? "-") + "\n"
|
||||
+ "- overBudgetCount: " + (apiDomLagSummary.overBudgetCount ?? 0) + "\n"
|
||||
+ "- lowConfidenceStreamOpenCount: " + (apiDomLagSummary.lowConfidenceStreamOpenCount ?? 0) + "\n"
|
||||
+ "- policy: 该指标是调查证据,不作为 Code Agent 阻塞项;/v1/workbench/events 只能代表 SSE stream-open 到后续 DOM 变化的低置信度候选。\n\n"
|
||||
+ "#### API-DOM groups\n\n" + apiDomLagGroupLines + "\n\n"
|
||||
+ "#### Worst API-DOM candidates\n\n" + apiDomLagWorstLines + "\n\n"
|
||||
+ "### Prompt network\n\n" + promptNetworkLines + "\n\n"
|
||||
+ "### Runtime alerts\n\n"
|
||||
+ "- httpErrorCount: " + (alertSummary.httpErrorCount ?? 0) + "\n"
|
||||
|
||||
Reference in New Issue
Block a user