fix: recover observer shell during workbench quick verify
This commit is contained in:
@@ -762,6 +762,7 @@ function compactSampleForAnalysis(sample) {
|
||||
observerInitiated: sample.observerInitiated ?? null,
|
||||
url: sample.url ?? null,
|
||||
path: sample.path ?? null,
|
||||
title: sample.title ?? null,
|
||||
routeSessionId: sample.routeSessionId ?? null,
|
||||
activeSessionId: sample.activeSessionId ?? null,
|
||||
messages: compactDomItems(sample.messages),
|
||||
@@ -770,6 +771,7 @@ function compactSampleForAnalysis(sample) {
|
||||
sessionRail: compactSessionRail(sample.sessionRail),
|
||||
turns: compactDomItems(sample.turns),
|
||||
diagnostics: compactDomItems(sample.diagnostics),
|
||||
composer: compactComposer(sample.composer),
|
||||
projectManagement: compactProjectManagementSample(sample.projectManagement),
|
||||
pageProvenance: compactSamplePageProvenance(sample.pageProvenance),
|
||||
performance: compactPerformanceItems(sample.performance)
|
||||
@@ -858,6 +860,20 @@ function compactSessionRail(value) {
|
||||
};
|
||||
}
|
||||
|
||||
function compactComposer(value) {
|
||||
if (!value || typeof value !== "object") return null;
|
||||
return {
|
||||
inputPresent: value.inputPresent === true,
|
||||
inputDisabled: value.inputDisabled === true,
|
||||
warningPresent: value.warningPresent === true,
|
||||
submitPresent: value.submitPresent === true,
|
||||
submitDisabled: value.submitDisabled === true,
|
||||
submitAction: value.submitAction ?? null,
|
||||
activeStatus: value.activeStatus ?? null,
|
||||
valuesRedacted: true
|
||||
};
|
||||
}
|
||||
|
||||
function compactLoadingItems(items) {
|
||||
if (!Array.isArray(items)) return [];
|
||||
return items.map((item) => {
|
||||
@@ -954,7 +970,8 @@ function compactPerformanceItems(items) {
|
||||
transferSize: item?.transferSize ?? null,
|
||||
encodedBodySize: item?.encodedBodySize ?? null,
|
||||
decodedBodySize: item?.decodedBodySize ?? null,
|
||||
nextHopProtocol: item?.nextHopProtocol ?? null
|
||||
nextHopProtocol: item?.nextHopProtocol ?? null,
|
||||
responseStatus: Number.isFinite(Number(item?.responseStatus)) ? Number(item.responseStatus) : null
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -1781,6 +1798,139 @@ function controlledNavigationHydrationCrossPageDiff(row, windows, sampleBySeq) {
|
||||
return isBlankHydrationProjectionSample(control) || isBlankHydrationProjectionSample(observer);
|
||||
}
|
||||
|
||||
function crossPageDiffHasWorkbenchAppShellNotReady(row, sampleBySeq) {
|
||||
const control = sampleBySeq.get(Number(row?.control?.seq));
|
||||
const observer = sampleBySeq.get(Number(row?.observer?.seq));
|
||||
return workbenchSampleAppShellNotReady(control) || workbenchSampleAppShellNotReady(observer);
|
||||
}
|
||||
|
||||
function workbenchSampleAppShellNotReady(sample) {
|
||||
if (!sample || !isWorkbenchPathSample(sample)) return false;
|
||||
const routeSessionId = stringOrNull(sample.routeSessionId) || workbenchSessionIdFromPath(samplePathname(sample));
|
||||
if (!routeSessionId) return false;
|
||||
const messageCount = Array.isArray(sample.messages) ? sample.messages.length : Number(sample.messageCount ?? 0);
|
||||
const traceRowCount = Array.isArray(sample.traceRows) ? sample.traceRows.length : Number(sample.traceRowCount ?? 0);
|
||||
const turnCount = Array.isArray(sample.turns) ? sample.turns.length : Number(sample.turnCount ?? 0);
|
||||
const loadingCount = Array.isArray(sample.loadings) ? sample.loadings.length : 0;
|
||||
const diagnosticCount = Array.isArray(sample.diagnostics) ? sample.diagnostics.length : 0;
|
||||
const railVisibleCount = Number(sample?.sessionRail?.visibleCount ?? 0);
|
||||
const composer = objectValue(sample.composer);
|
||||
const composerPresent = composer.inputPresent === true || composer.submitPresent === true;
|
||||
const provenance = objectValue(sample.pageProvenance);
|
||||
const hasWorkbenchAssets = Number(provenance.scriptCount ?? 0) > 0 || Number(provenance.stylesheetCount ?? 0) > 0;
|
||||
return !stringOrNull(sample.activeSessionId)
|
||||
&& Number(messageCount) === 0
|
||||
&& Number(traceRowCount) === 0
|
||||
&& Number(turnCount) === 0
|
||||
&& Number(loadingCount) === 0
|
||||
&& Number(diagnosticCount) === 0
|
||||
&& Number(railVisibleCount) === 0
|
||||
&& !composerPresent
|
||||
&& hasWorkbenchAssets;
|
||||
}
|
||||
|
||||
function detectWorkbenchAppShellNotReady(samples) {
|
||||
const rows = (Array.isArray(samples) ? samples : [])
|
||||
.filter(workbenchSampleAppShellNotReady)
|
||||
.map((sample) => workbenchAppShellNotReadyRef(sample));
|
||||
return annotateWorkbenchAppShellNotReadyTiming(rows);
|
||||
}
|
||||
|
||||
function annotateWorkbenchAppShellNotReadyTiming(rows) {
|
||||
const groups = new Map();
|
||||
const splitGapMs = Math.max(1000, Number(alertThresholds.crossPageProjectionDivergenceRedMs || alertThresholds.visibleLoadingSlowMs || 10_000));
|
||||
for (const row of rows.slice().sort((a, b) => timestampMs(a.ts) - timestampMs(b.ts))) {
|
||||
const ms = timestampMs(row.ts);
|
||||
const key = [row.pageRole || "control", row.pageId || "default", row.routeSessionId || row.url || ""].join(":");
|
||||
const group = groups.get(key) || [];
|
||||
let segment = group.at(-1);
|
||||
if (!segment || !Number.isFinite(ms) || !Number.isFinite(segment.lastMs) || ms - segment.lastMs > splitGapMs) {
|
||||
segment = { rows: [], firstMs: Number.isFinite(ms) ? ms : null, lastMs: Number.isFinite(ms) ? ms : null };
|
||||
group.push(segment);
|
||||
}
|
||||
segment.rows.push(row);
|
||||
if (Number.isFinite(ms)) {
|
||||
if (segment.firstMs === null || ms < segment.firstMs) segment.firstMs = ms;
|
||||
if (segment.lastMs === null || ms > segment.lastMs) segment.lastMs = ms;
|
||||
}
|
||||
groups.set(key, group);
|
||||
}
|
||||
const result = [];
|
||||
for (const group of groups.values()) {
|
||||
for (let segmentIndex = 0; segmentIndex < group.length; segmentIndex += 1) {
|
||||
const segment = group[segmentIndex];
|
||||
const observedSpanMs = segment.firstMs === null || segment.lastMs === null ? null : segment.lastMs - segment.firstMs;
|
||||
for (const row of segment.rows) {
|
||||
result.push({
|
||||
...row,
|
||||
segmentIndex,
|
||||
observedFirstAt: segment.firstMs === null ? null : new Date(segment.firstMs).toISOString(),
|
||||
observedLastAt: segment.lastMs === null ? null : new Date(segment.lastMs).toISOString(),
|
||||
observedSpanMs,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function workbenchAppShellNotReadyRef(sample) {
|
||||
const provenance = objectValue(sample?.pageProvenance);
|
||||
const performance = workbenchAssetPerformanceSummary(sample);
|
||||
return {
|
||||
...ref(sample),
|
||||
title: stringOrNull(sample?.title),
|
||||
messageCount: Array.isArray(sample?.messages) ? sample.messages.length : 0,
|
||||
turnCount: Array.isArray(sample?.turns) ? sample.turns.length : 0,
|
||||
traceRowCount: Array.isArray(sample?.traceRows) ? sample.traceRows.length : 0,
|
||||
sessionRailVisibleCount: Number(sample?.sessionRail?.visibleCount ?? 0),
|
||||
composerInputPresent: sample?.composer?.inputPresent === true,
|
||||
composerSubmitPresent: sample?.composer?.submitPresent === true,
|
||||
pageProvenance: {
|
||||
documentReadyState: stringOrNull(provenance.documentReadyState),
|
||||
pageLoadSeq: numberOrNull(provenance.pageLoadSeq),
|
||||
timeOrigin: numberOrNull(provenance.timeOrigin),
|
||||
assetFingerprint: stringOrNull(provenance.assetFingerprint),
|
||||
scriptCount: numberOrNull(provenance.scriptCount),
|
||||
stylesheetCount: numberOrNull(provenance.stylesheetCount),
|
||||
scripts: arrayStrings(provenance.scripts).slice(0, 8),
|
||||
stylesheets: arrayStrings(provenance.stylesheets).slice(0, 8),
|
||||
valuesRedacted: true
|
||||
},
|
||||
assetPerformance: performance,
|
||||
valuesRedacted: true
|
||||
};
|
||||
}
|
||||
|
||||
function workbenchAssetPerformanceSummary(sample) {
|
||||
const assets = (Array.isArray(sample?.performance) ? sample.performance : [])
|
||||
.filter((entry) => /^(script|link|css)$/iu.test(String(entry?.initiatorType || "")) || /\.(?:js|css)$/iu.test(String(entry?.name || "")))
|
||||
.map((entry) => ({
|
||||
path: urlPath(entry?.name),
|
||||
initiatorType: entry?.initiatorType ?? null,
|
||||
duration: numberOrNull(entry?.duration),
|
||||
responseStatus: numberOrNull(entry?.responseStatus),
|
||||
transferSize: numberOrNull(entry?.transferSize),
|
||||
encodedBodySize: numberOrNull(entry?.encodedBodySize),
|
||||
decodedBodySize: numberOrNull(entry?.decodedBodySize),
|
||||
nextHopProtocol: entry?.nextHopProtocol ?? null,
|
||||
valuesRedacted: true
|
||||
}));
|
||||
const responseStatusCounts = {};
|
||||
for (const item of assets) {
|
||||
const key = item.responseStatus === null ? "unknown" : String(item.responseStatus);
|
||||
responseStatusCounts[key] = (responseStatusCounts[key] || 0) + 1;
|
||||
}
|
||||
return {
|
||||
assetCount: assets.length,
|
||||
zeroStatusCount: assets.filter((item) => item.responseStatus === 0).length,
|
||||
missingStatusCount: assets.filter((item) => item.responseStatus === null).length,
|
||||
responseStatusCounts,
|
||||
assets: assets.slice(0, 12),
|
||||
valuesRedacted: true
|
||||
};
|
||||
}
|
||||
|
||||
function objectValue(value) {
|
||||
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
||||
}
|
||||
@@ -2735,14 +2885,32 @@ function buildFindings(samples, control, network, errors, sampleMetrics, promptN
|
||||
const crossPageTraceVisibilityDiffs = crossPageDiffs.filter((item) => item.diffKind === "trace-visibility");
|
||||
const crossPageProjectionBudgetMs = alertThresholds.crossPageProjectionDivergenceRedMs;
|
||||
const sampleBySeq = new Map(samples.map((item) => [Number(item?.seq), item]).filter(([seq]) => Number.isFinite(seq)));
|
||||
const appShellNotReadyRows = detectWorkbenchAppShellNotReady(samples);
|
||||
const persistentAppShellNotReadyRows = appShellNotReadyRows.filter((item) => Number(item.observedSpanMs ?? 0) > crossPageProjectionBudgetMs);
|
||||
const transientAppShellNotReadyRows = appShellNotReadyRows.filter((item) => Number(item.observedSpanMs ?? 0) <= crossPageProjectionBudgetMs);
|
||||
if (persistentAppShellNotReadyRows.length > 0) findings.push({
|
||||
id: "workbench-app-shell-not-ready",
|
||||
severity: "red",
|
||||
summary: "Workbench route document loaded but the app shell never mounted or hydrated within the configured budget; treat this as page/runtime startup evidence, not a Workbench projection divergence",
|
||||
count: persistentAppShellNotReadyRows.length,
|
||||
budgetMs: crossPageProjectionBudgetMs,
|
||||
samples: persistentAppShellNotReadyRows.slice(0, 20),
|
||||
rootCause: "workbench_page_app_shell_not_ready",
|
||||
rootCauseStatus: "confirmed-from-dom-and-asset-provenance",
|
||||
rootCauseConfidence: "high",
|
||||
valuesRedacted: true
|
||||
});
|
||||
if (transientAppShellNotReadyRows.length > 0) findings.push({ id: "workbench-app-shell-transient-not-ready", severity: "info", summary: "Workbench route briefly had a document and assets but no mounted app shell; retained as startup context", count: transientAppShellNotReadyRows.length, budgetMs: crossPageProjectionBudgetMs, samples: transientAppShellNotReadyRows.slice(0, 20) });
|
||||
const timedCrossPageProjectionDiffs = annotateCrossPageDiffTiming(crossPageProjectionDiffs);
|
||||
const controlledNavigationHydrationProjectionDiffs = timedCrossPageProjectionDiffs.filter((item) => controlledNavigationHydrationCrossPageDiff(item, controlledNavigationWindows, sampleBySeq));
|
||||
const evaluatedCrossPageProjectionDiffs = timedCrossPageProjectionDiffs.filter((item) => !controlledNavigationHydrationCrossPageDiff(item, controlledNavigationWindows, sampleBySeq));
|
||||
const appShellNotReadyProjectionDiffs = timedCrossPageProjectionDiffs.filter((item) => !controlledNavigationHydrationCrossPageDiff(item, controlledNavigationWindows, sampleBySeq) && crossPageDiffHasWorkbenchAppShellNotReady(item, sampleBySeq));
|
||||
const evaluatedCrossPageProjectionDiffs = timedCrossPageProjectionDiffs.filter((item) => !controlledNavigationHydrationCrossPageDiff(item, controlledNavigationWindows, sampleBySeq) && !crossPageDiffHasWorkbenchAppShellNotReady(item, sampleBySeq));
|
||||
const persistentCrossPageProjectionDiffs = evaluatedCrossPageProjectionDiffs.filter((item) => Number(item.observedSpanMs ?? 0) > crossPageProjectionBudgetMs);
|
||||
const transientCrossPageProjectionDiffs = evaluatedCrossPageProjectionDiffs.filter((item) => Number(item.observedSpanMs ?? 0) <= crossPageProjectionBudgetMs);
|
||||
if (persistentCrossPageProjectionDiffs.length > 0) findings.push({ id: "cross-page-projection-divergence", severity: "red", summary: "control and observer pages saw different projection state for the same sampled session beyond the configured budget", count: persistentCrossPageProjectionDiffs.length, budgetMs: crossPageProjectionBudgetMs, samples: persistentCrossPageProjectionDiffs.slice(0, 20) });
|
||||
if (transientCrossPageProjectionDiffs.length > 0) findings.push({ id: "cross-page-projection-transient-divergence", severity: "info", summary: "control and observer pages briefly differed near a sampled transition; retained as transient evidence but not treated as persistent projection failure", count: transientCrossPageProjectionDiffs.length, budgetMs: crossPageProjectionBudgetMs, samples: transientCrossPageProjectionDiffs.slice(0, 20) });
|
||||
if (controlledNavigationHydrationProjectionDiffs.length > 0) findings.push({ id: "cross-page-projection-controlled-navigation-hydration", severity: "info", summary: "control and observer pages differed while a non-blocking session-invariance navigation command still had an unhydrated blank page; retained as context but not treated as a red projection blocker", count: controlledNavigationHydrationProjectionDiffs.length, budgetMs: crossPageProjectionBudgetMs, samples: controlledNavigationHydrationProjectionDiffs.slice(0, 20) });
|
||||
if (appShellNotReadyProjectionDiffs.length > 0) findings.push({ id: "cross-page-projection-app-shell-not-ready", severity: "info", summary: "cross-page projection differences were explained by a page whose Workbench app shell was not mounted; see workbench-app-shell-not-ready for the blocking root cause", count: appShellNotReadyProjectionDiffs.length, budgetMs: crossPageProjectionBudgetMs, samples: appShellNotReadyProjectionDiffs.slice(0, 20) });
|
||||
if (crossPageTraceVisibilityDiffs.length > 0) findings.push({ id: "cross-page-trace-visibility-divergence", severity: "info", summary: "control and observer pages differed only in visible trace row count; this is local disclosure/hydration visibility, not session/message projection divergence", count: crossPageTraceVisibilityDiffs.length, samples: crossPageTraceVisibilityDiffs.slice(0, 20) });
|
||||
const traceMessageDuplicates = detectTraceMessageDuplication(samples);
|
||||
if (traceMessageDuplicates.length > 0) findings.push({ id: "trace-assistant-message-duplicates-final-response", severity: "amber", summary: "trace-frame rendered duplicate visible assistant final rows; the fixed Final Response renderer summary block is excluded", count: traceMessageDuplicates.length, finalResponseSummaryBlockCounted: false, traceFrameSource: "traceRows-only", samples: traceMessageDuplicates.slice(0, 20) });
|
||||
|
||||
Reference in New Issue
Block a user