fix(web-probe): alert session title fallback majority (#705)
Co-authored-by: Codex <codex@noreply.local>
This commit is contained in:
@@ -8100,7 +8100,14 @@ function runNodeWebProbeObserveAnalyze(options: NodeWebProbeObserveOptions, spec
|
||||
" loadingLongestContinuousSeconds: compact.sampleMetrics.loadingLongestContinuousSeconds ?? null,",
|
||||
" loadingCurrentContinuousSeconds: compact.sampleMetrics.loadingCurrentContinuousSeconds ?? null,",
|
||||
" loadingOverFiveSecondSegmentCount: compact.sampleMetrics.loadingOverFiveSecondSegmentCount ?? null,",
|
||||
" sessionRailSampleCount: compact.sampleMetrics.sessionRailSampleCount ?? null,",
|
||||
" sessionRailVisibleSampleCount: compact.sampleMetrics.sessionRailVisibleSampleCount ?? null,",
|
||||
" sessionRailFallbackMajoritySampleCount: compact.sampleMetrics.sessionRailFallbackMajoritySampleCount ?? null,",
|
||||
" sessionRailFallbackMaxRatio: compact.sampleMetrics.sessionRailFallbackMaxRatio ?? null,",
|
||||
" sessionRailFallbackMaxVisibleCount: compact.sampleMetrics.sessionRailFallbackMaxVisibleCount ?? null,",
|
||||
" sessionRailFallbackMaxCount: compact.sampleMetrics.sessionRailFallbackMaxCount ?? null,",
|
||||
" loading: compact.sampleMetrics.loading ?? null,",
|
||||
" sessionRailTitles: compact.sampleMetrics.sessionRailTitles ? { summary: compact.sampleMetrics.sessionRailTitles.summary ?? null, samples: Array.isArray(compact.sampleMetrics.sessionRailTitles.samples) ? compact.sampleMetrics.sessionRailTitles.samples.slice(0, 4) : [], examples: Array.isArray(compact.sampleMetrics.sessionRailTitles.examples) ? compact.sampleMetrics.sessionRailTitles.examples.slice(0, 4) : [] } : null,",
|
||||
" turnTimingRows: compact.sampleMetrics.turnTimingRows ?? null,",
|
||||
" turnTimingNonMonotonicCount: compact.sampleMetrics.turnTimingNonMonotonicCount ?? null,",
|
||||
" turnTimingTotalElapsedDecreaseCount: compact.sampleMetrics.turnTimingTotalElapsedDecreaseCount ?? null,",
|
||||
@@ -8151,6 +8158,10 @@ function runNodeWebProbeObserveAnalyze(options: NodeWebProbeObserveOptions, spec
|
||||
" loadingCurrentContinuousSeconds: compact.sampleMetrics.loadingCurrentContinuousSeconds ?? null,",
|
||||
" loadingOverFiveSecondSegmentCount: compact.sampleMetrics.loadingOverFiveSecondSegmentCount ?? null,",
|
||||
" loading: compact.sampleMetrics.loading ? { summary: compact.sampleMetrics.loading.summary ?? null, longestSegments: Array.isArray(compact.sampleMetrics.loading.longestSegments) ? compact.sampleMetrics.loading.longestSegments.slice(0, 4) : [], owners: Array.isArray(compact.sampleMetrics.loading.owners) ? compact.sampleMetrics.loading.owners.slice(0, 4) : [] } : null,",
|
||||
" sessionRailFallbackMajoritySampleCount: compact.sampleMetrics.sessionRailFallbackMajoritySampleCount ?? null,",
|
||||
" sessionRailFallbackMaxRatio: compact.sampleMetrics.sessionRailFallbackMaxRatio ?? null,",
|
||||
" sessionRailFallbackMaxCount: compact.sampleMetrics.sessionRailFallbackMaxCount ?? null,",
|
||||
" sessionRailTitles: compact.sampleMetrics.sessionRailTitles ? { summary: compact.sampleMetrics.sessionRailTitles.summary ?? null, samples: Array.isArray(compact.sampleMetrics.sessionRailTitles.samples) ? compact.sampleMetrics.sessionRailTitles.samples.slice(0, 4) : [], examples: Array.isArray(compact.sampleMetrics.sessionRailTitles.examples) ? compact.sampleMetrics.sessionRailTitles.examples.slice(0, 4) : [] } : null,",
|
||||
" turnTimingRows: compact.sampleMetrics.turnTimingRows ?? null,",
|
||||
" turnTimingNonMonotonicCount: compact.sampleMetrics.turnTimingNonMonotonicCount ?? null,",
|
||||
" turnTimingTotalElapsedDecreaseCount: compact.sampleMetrics.turnTimingTotalElapsedDecreaseCount ?? null,",
|
||||
@@ -8224,6 +8235,10 @@ function renderWebObserveAnalyzeTable(value: Record<string, unknown>): string {
|
||||
const loadingSummary = record(loading?.summary);
|
||||
const loadingSegments = Array.isArray(loading?.longestSegments) ? loading.longestSegments.map(record).filter((item): item is Record<string, unknown> => item !== null).slice(0, 8) : [];
|
||||
const loadingOwners = Array.isArray(loading?.owners) ? loading.owners.map(record).filter((item): item is Record<string, unknown> => item !== null).slice(0, 8) : [];
|
||||
const sessionRailTitles = record(sampleMetrics?.sessionRailTitles);
|
||||
const sessionRailTitleSummary = record(sessionRailTitles?.summary);
|
||||
const sessionRailTitleSamples = Array.isArray(sessionRailTitles?.samples) ? sessionRailTitles.samples.map(record).filter((item): item is Record<string, unknown> => item !== null).slice(0, 8) : [];
|
||||
const sessionRailTitleExamples = Array.isArray(sessionRailTitles?.examples) ? sessionRailTitles.examples.map(record).filter((item): item is Record<string, unknown> => item !== null).slice(0, 8) : [];
|
||||
const rounds = webObserveArray(sampleMetrics?.roundItems ?? sampleMetrics?.rounds).map(record).filter((item): item is Record<string, unknown> => item !== null).slice(-8);
|
||||
const turnColumns = webObserveArray(sampleMetrics?.turnColumns).map(record).filter((item): item is Record<string, unknown> => item !== null).slice(-12);
|
||||
const roundCount = Array.isArray(sampleMetrics?.rounds) ? sampleMetrics.rounds.length : sampleMetrics?.rounds;
|
||||
@@ -8428,6 +8443,31 @@ function renderWebObserveAnalyzeTable(value: Record<string, unknown>): string {
|
||||
webObserveText(loadingSummary?.overFiveSecondSegmentCount ?? sampleMetrics?.loadingOverFiveSecondSegmentCount),
|
||||
]]),
|
||||
"",
|
||||
"Session rail titles:",
|
||||
webObserveTable(["SAMPLES", "VISIBLE", "MAJORITY", "MAX_RATIO", "MAX_VISIBLE", "MAX_FALLBACK"], [[
|
||||
webObserveText(sessionRailTitleSummary?.sampleCount ?? sampleMetrics?.sessionRailSampleCount),
|
||||
webObserveText(sessionRailTitleSummary?.visibleSampleCount ?? sampleMetrics?.sessionRailVisibleSampleCount),
|
||||
webObserveText(sessionRailTitleSummary?.majorityFallbackSampleCount ?? sampleMetrics?.sessionRailFallbackMajoritySampleCount),
|
||||
webObserveText(sessionRailTitleSummary?.maxFallbackRatio ?? sampleMetrics?.sessionRailFallbackMaxRatio),
|
||||
webObserveText(sessionRailTitleSummary?.maxVisibleCount ?? sampleMetrics?.sessionRailFallbackMaxVisibleCount),
|
||||
webObserveText(sessionRailTitleSummary?.maxFallbackTitleCount ?? sampleMetrics?.sessionRailFallbackMaxCount),
|
||||
]]),
|
||||
sessionRailTitleSamples.length === 0 ? "Session rail fallback samples:\n-" : webObserveTable(["SEQ", "ROLE", "VISIBLE", "FALLBACK", "RATIO", "EXAMPLE"], sessionRailTitleSamples.map((item) => [
|
||||
webObserveText(item.seq),
|
||||
webObserveText(item.pageRole),
|
||||
webObserveText(item.visibleCount),
|
||||
webObserveText(item.fallbackTitleCount),
|
||||
webObserveText(item.fallbackTitleRatio),
|
||||
webObserveShort(webObserveText((Array.isArray(item.examples) ? record(item.examples[0])?.titlePreview : null) ?? ""), 48),
|
||||
])),
|
||||
sessionRailTitleExamples.length === 0 ? "Session rail fallback examples:\n-" : webObserveTable(["SEQ", "ROLE", "ACTIVE", "SESSION", "TITLE"], sessionRailTitleExamples.map((item) => [
|
||||
webObserveText(item.firstSeq),
|
||||
webObserveText(item.pageRole),
|
||||
webObserveText(item.active),
|
||||
webObserveText(item.sessionIdPrefix),
|
||||
webObserveShort(webObserveText(item.titlePreview ?? item.titleHash), 64),
|
||||
])),
|
||||
"",
|
||||
"Loading owners:",
|
||||
webObserveTable(["SAMPLES", "OCCUR", "MAX", "LONGEST_S", "KIND", "OWNER"], loadingOwners.length > 0 ? loadingOwners.map((item) => [
|
||||
webObserveText(item.sampleCount),
|
||||
@@ -9237,7 +9277,7 @@ const compactHeartbeat=(item)=>item?{ok:item.ok,jobId:item.jobId,pid:item.pid,st
|
||||
const retryLabel=(detail)=>detail&&detail.auth?detail.auth.lastRetryLabel||'':detail&&detail.result?detail.result.lastRetryLabel||'':detail&&detail.error&&detail.error.auth?detail.error.auth.lastRetryLabel||'':'';
|
||||
const detailText=(detail)=>detail&&detail.error?short((detail.error.message||'')+(detail.error.auth&&detail.error.auth.lastError?' '+detail.error.auth.lastError:'')):detail&&detail.result?short([detail.result.statusText,detail.result.retryExhausted?'retry-exhausted':''].filter(Boolean).join(' ')):'';
|
||||
const compactControl=(item)=>({ts:item.ts,seq:item.seq,phase:item.phase,type:item.type,commandId:item.commandId,durationMs:item.detail&&item.detail.durationMs||null,retry:retryLabel(item.detail),detail:detailText(item.detail)});
|
||||
const compactSample=(item)=>({seq:item.seq,ts:item.ts,path:item.path,routeSessionId:item.routeSessionId||null,activeSessionId:item.activeSessionId||null,messageCount:Array.isArray(item.messages)?item.messages.length:0,traceRowCount:Array.isArray(item.traceRows)?item.traceRows.length:0,loadingCount:Array.isArray(item.loadings)?item.loadings.length:0,loadingOwners:Array.isArray(item.loadings)?Array.from(new Set(item.loadings.map((loading)=>loading.ownerLabel||loading.ownerKind||loading.ownerKey||'loading'))).slice(0,4):[]});
|
||||
const compactSample=(item)=>({seq:item.seq,ts:item.ts,path:item.path,routeSessionId:item.routeSessionId||null,activeSessionId:item.activeSessionId||null,messageCount:Array.isArray(item.messages)?item.messages.length:0,traceRowCount:Array.isArray(item.traceRows)?item.traceRows.length:0,loadingCount:Array.isArray(item.loadings)?item.loadings.length:0,loadingOwners:Array.isArray(item.loadings)?Array.from(new Set(item.loadings.map((loading)=>loading.ownerLabel||loading.ownerKind||loading.ownerKey||'loading'))).slice(0,4):[],sessionRail:item.sessionRail?{visibleCount:item.sessionRail.visibleCount??null,fallbackTitleCount:item.sessionRail.fallbackTitleCount??null,fallbackTitleRatio:item.sessionRail.fallbackTitleRatio??null,fallbackItems:Array.isArray(item.sessionRail.fallbackItems)?item.sessionRail.fallbackItems.slice(0,4).map((fallback)=>({sessionIdPrefix:fallback.sessionIdPrefix??null,titlePreview:fallback.titlePreview??null,active:fallback.active===true})):[]}:null});
|
||||
const compactNetwork=(item)=>({ts:item.ts,type:item.type,method:item.method,url:short(item.url),status:item.status||null,failure:item.failure?short(item.failure):null});
|
||||
const pidText=fs.existsSync(path.join(dir,'pid'))?fs.readFileSync(path.join(dir,'pid'),'utf8').trim():null;
|
||||
let alive=false; if(pidText){try{process.kill(Number(pidText),0); alive=true}catch{}}
|
||||
|
||||
@@ -1338,6 +1338,78 @@ async function sampleOnePage(targetPage, { reason, groupSeq, pageRole, targetPag
|
||||
};
|
||||
});
|
||||
};
|
||||
const sessionIdForElement = (element) => {
|
||||
const direct = element.getAttribute("data-session-id");
|
||||
if (direct) return direct;
|
||||
const href = element.getAttribute("href") || element.closest("a[href]")?.getAttribute("href") || "";
|
||||
if (!href) return null;
|
||||
try {
|
||||
const parsed = new URL(href, location.href);
|
||||
const match = parsed.pathname.match(/\/(?:workbench|workspace)\/sessions\/([^/?#]+)/u);
|
||||
return match ? decodeURIComponent(match[1] || "") : null;
|
||||
} catch {
|
||||
const match = href.match(/\/(?:workbench|workspace)\/sessions\/([^/?#]+)/u);
|
||||
return match ? decodeURIComponent(match[1] || "") : null;
|
||||
}
|
||||
};
|
||||
const sessionTitleTextForElement = (element) => {
|
||||
const titleNode = element.querySelector("[data-testid*='session-title' i], [data-testid*='session-name' i], [class*='session-title' i], [class*='session-name' i], [data-testid*='title' i], [class*='title' i]");
|
||||
return trim(
|
||||
(titleNode ? titleNode.textContent || "" : "")
|
||||
|| element.getAttribute("aria-label")
|
||||
|| element.getAttribute("title")
|
||||
|| element.textContent
|
||||
|| "",
|
||||
240
|
||||
);
|
||||
};
|
||||
const sessionTitleFallbackPattern = /^(?:Session\s+)?ses_[A-Za-z0-9_.-]+/iu;
|
||||
const looksLikeSessionTitleFallback = (title, sessionId) => {
|
||||
const text = trim(title, 240);
|
||||
const id = String(sessionId || "").trim();
|
||||
if (!text) return true;
|
||||
if (sessionTitleFallbackPattern.test(text)) return true;
|
||||
if (!id) return false;
|
||||
return text === id || text.startsWith(id) || text.startsWith("Session " + id);
|
||||
};
|
||||
const collectSessionRailTitles = () => {
|
||||
const candidates = Array.from(document.querySelectorAll(".session-tab[data-session-id], [role='tab'][data-session-id], [data-testid*='session' i][data-session-id], a[href*='/workbench/sessions/'], a[href*='/workspace/sessions/']"))
|
||||
.filter(visible);
|
||||
const seen = new Set();
|
||||
const items = [];
|
||||
for (const element of candidates) {
|
||||
const sessionId = sessionIdForElement(element);
|
||||
const titleText = sessionTitleTextForElement(element);
|
||||
const key = (sessionId || "") + "|" + titleText;
|
||||
if (seen.has(key)) continue;
|
||||
seen.add(key);
|
||||
const rect = element.getBoundingClientRect();
|
||||
const fallbackTitle = looksLikeSessionTitleFallback(titleText, sessionId);
|
||||
items.push({
|
||||
index: items.length,
|
||||
tag: element.tagName.toLowerCase(),
|
||||
testId: element.getAttribute("data-testid"),
|
||||
role: element.getAttribute("role"),
|
||||
active: element.getAttribute("data-active") === "true" || element.getAttribute("aria-selected") === "true",
|
||||
sessionId,
|
||||
sessionIdPrefix: sessionId ? String(sessionId).slice(0, 12) : null,
|
||||
titleText,
|
||||
fallbackTitle,
|
||||
rect: { x: Math.round(rect.x), y: Math.round(rect.y), width: Math.round(rect.width), height: Math.round(rect.height) },
|
||||
});
|
||||
}
|
||||
const visibleItems = items.slice(0, 120);
|
||||
const fallbackItems = visibleItems.filter((item) => item.fallbackTitle);
|
||||
const visibleCount = visibleItems.length;
|
||||
const fallbackTitleCount = fallbackItems.length;
|
||||
return {
|
||||
visibleCount,
|
||||
fallbackTitleCount,
|
||||
fallbackTitleRatio: visibleCount > 0 ? Number((fallbackTitleCount / visibleCount).toFixed(4)) : 0,
|
||||
items: visibleItems.slice(0, 60),
|
||||
fallbackItems: fallbackItems.slice(0, 12),
|
||||
};
|
||||
};
|
||||
const diagnosticSummaryText = (element) => {
|
||||
const summarySelectors = [
|
||||
".api-error-diagnostic-summary-text p",
|
||||
@@ -1424,6 +1496,7 @@ async function sampleOnePage(targetPage, { reason, groupSeq, pageRole, targetPag
|
||||
const messages = summarize(messageSelector, 20);
|
||||
const traceRows = summarize(traceSelector, 30);
|
||||
const loadings = collectLoadingNodes();
|
||||
const sessionRail = collectSessionRailTitles();
|
||||
const diagnostics = Array.from(document.querySelectorAll(diagnosticSelector)).filter(visible).slice(-40).map((element, index) => {
|
||||
const rect = element.getBoundingClientRect();
|
||||
const text = diagnosticSummaryText(element);
|
||||
@@ -1488,6 +1561,7 @@ async function sampleOnePage(targetPage, { reason, groupSeq, pageRole, targetPag
|
||||
messages,
|
||||
traceRows,
|
||||
loadings,
|
||||
sessionRail,
|
||||
diagnostics,
|
||||
turns,
|
||||
pageProvenance: {
|
||||
@@ -1543,11 +1617,44 @@ function digestDom(dom, pageRole = "control") {
|
||||
const messages = Array.isArray(dom.messages) ? dom.messages.map((item) => ({ ...item, textHash: sha256Text(item.text || ""), textPreview: truncate(item.text || "", 160), textBytes: Buffer.byteLength(item.text || "") })) : [];
|
||||
const traceRows = Array.isArray(dom.traceRows) ? dom.traceRows.map((item) => ({ ...item, textHash: sha256Text(item.text || ""), textPreview: truncate(item.text || "", 160), textBytes: Buffer.byteLength(item.text || "") })) : [];
|
||||
const loadings = Array.isArray(dom.loadings) ? dom.loadings.map((item) => ({ ...item, textHash: sha256Text(item.text || ""), textPreview: truncate(item.text || "", 160), textBytes: Buffer.byteLength(item.text || ""), ownerTextHash: sha256Text(item.ownerText || ""), ownerTextPreview: truncate(item.ownerText || "", 160) })) : [];
|
||||
const sessionRail = digestSessionRail(dom.sessionRail);
|
||||
const diagnostics = Array.isArray(dom.diagnostics) ? dom.diagnostics.map((item) => ({ ...item, textHash: sha256Text(item.text || ""), textPreview: truncate(item.text || "", 260), textBytes: Buffer.byteLength(item.text || "") })) : [];
|
||||
const turns = Array.isArray(dom.turns) ? dom.turns.map((item) => ({ ...item, textHash: sha256Text(item.text || ""), textPreview: truncate(item.text || "", 200), textBytes: Buffer.byteLength(item.text || "") })) : [];
|
||||
const pageProvenance = normalizePageProvenance(dom.pageProvenance, { reason: "sample", pageLoadSeq: currentPageProvenance?.pageLoadSeq ?? pageLoadSeq });
|
||||
if (pageRole === "control") currentPageProvenance = pageProvenance;
|
||||
return { ...dom, messages, traceRows, loadings, diagnostics, turns, pageProvenance: compactPageProvenance(pageProvenance) };
|
||||
return { ...dom, messages, traceRows, loadings, sessionRail, diagnostics, turns, pageProvenance: compactPageProvenance(pageProvenance) };
|
||||
}
|
||||
|
||||
function digestSessionRail(value) {
|
||||
if (!value || typeof value !== "object") return null;
|
||||
const items = Array.isArray(value.items) ? value.items.map((item) => {
|
||||
const titleText = String(item?.titleText || item?.titlePreview || "");
|
||||
return {
|
||||
index: item?.index ?? null,
|
||||
tag: item?.tag ?? null,
|
||||
testId: item?.testId ?? null,
|
||||
role: item?.role ?? null,
|
||||
active: item?.active === true,
|
||||
sessionId: item?.sessionId ?? null,
|
||||
sessionIdPrefix: item?.sessionIdPrefix ?? (item?.sessionId ? String(item.sessionId).slice(0, 12) : null),
|
||||
fallbackTitle: item?.fallbackTitle === true,
|
||||
titleHash: sha256Text(titleText),
|
||||
titlePreview: truncate(titleText, 160),
|
||||
titleBytes: Buffer.byteLength(titleText),
|
||||
rect: item?.rect ?? null,
|
||||
};
|
||||
}) : [];
|
||||
const fallbackItems = items.filter((item) => item.fallbackTitle).slice(0, 12);
|
||||
const visibleCount = Number(value.visibleCount ?? items.length);
|
||||
const fallbackTitleCount = Number(value.fallbackTitleCount ?? fallbackItems.length);
|
||||
return {
|
||||
visibleCount: Number.isFinite(visibleCount) ? visibleCount : items.length,
|
||||
fallbackTitleCount: Number.isFinite(fallbackTitleCount) ? fallbackTitleCount : fallbackItems.length,
|
||||
fallbackTitleRatio: Number.isFinite(Number(value.fallbackTitleRatio)) ? Number(value.fallbackTitleRatio) : (items.length > 0 ? Number((fallbackItems.length / items.length).toFixed(4)) : 0),
|
||||
items,
|
||||
fallbackItems,
|
||||
valuesRedacted: true,
|
||||
};
|
||||
}
|
||||
|
||||
async function captureScreenshot(reason, imageType = "png") {
|
||||
@@ -1839,6 +1946,7 @@ console.log(JSON.stringify({
|
||||
rounds: recentWindow.sampleMetrics.rounds.slice(-8).map((item) => ({ promptIndex: item.promptIndex, promptTextHash: item.promptTextHash, sampleCount: item.sampleCount, firstSeq: item.firstSeq, lastSeq: item.lastSeq, lastTotalElapsedSeconds: item.lastTotalElapsedSeconds, lastRecentUpdateSeconds: item.lastRecentUpdateSeconds, loadingSamples: item.loadingSamples, maxLoadingCount: item.maxLoadingCount, loadingOwnerCount: item.loadingOwnerCount, diagnosticSamples: item.diagnosticSamples, terminalSamples: item.terminalSamples, finalTextSamples: item.finalTextSamples, turnTimingTotalElapsedZeroResetCount: item.turnTimingTotalElapsedZeroResetCount, turnTimingTotalElapsedForwardJumpCount: item.turnTimingTotalElapsedForwardJumpCount, turnTimingTotalElapsedForwardJumpMaxSeconds: item.turnTimingTotalElapsedForwardJumpMaxSeconds, turnTimingRecentUpdateJumpCount: item.turnTimingRecentUpdateJumpCount, turnTimingRecentUpdateMaxIncreaseSeconds: item.turnTimingRecentUpdateMaxIncreaseSeconds })),
|
||||
turnColumns: recentWindow.sampleMetrics.turnColumns.slice(-12).map((item) => ({ label: item.label, source: item.source, promptIndex: item.promptIndex, lastPromptIndex: item.lastPromptIndex, firstSeq: item.firstSeq, lastSeq: item.lastSeq, traceId: item.traceId, messageId: item.messageId })),
|
||||
loading: compactLoadingMetricsForOutput(recentWindow.sampleMetrics.loading),
|
||||
sessionRailTitles: compactSessionRailTitleMetricsForOutput(recentWindow.sampleMetrics.sessionRailTitles),
|
||||
},
|
||||
pageProvenance: recentWindow.pageProvenance.summary,
|
||||
pagePerformance: recentWindow.pagePerformance.summary,
|
||||
@@ -2011,6 +2119,7 @@ function compactSampleForAnalysis(sample) {
|
||||
messages: compactDomItems(sample.messages),
|
||||
traceRows: compactDomItems(sample.traceRows),
|
||||
loadings: compactLoadingItems(sample.loadings),
|
||||
sessionRail: compactSessionRail(sample.sessionRail),
|
||||
turns: compactDomItems(sample.turns),
|
||||
diagnostics: compactDomItems(sample.diagnostics),
|
||||
pageProvenance: compactSamplePageProvenance(sample.pageProvenance),
|
||||
@@ -2018,6 +2127,41 @@ function compactSampleForAnalysis(sample) {
|
||||
};
|
||||
}
|
||||
|
||||
function compactSessionRail(value) {
|
||||
if (!value || typeof value !== "object") return null;
|
||||
const items = Array.isArray(value.items) ? value.items.slice(0, 80).map((item) => ({
|
||||
index: item?.index ?? null,
|
||||
tag: item?.tag ?? null,
|
||||
testId: item?.testId ?? null,
|
||||
role: item?.role ?? null,
|
||||
active: item?.active === true,
|
||||
sessionIdPrefix: item?.sessionIdPrefix ?? (item?.sessionId ? String(item.sessionId).slice(0, 12) : null),
|
||||
fallbackTitle: item?.fallbackTitle === true,
|
||||
titlePreview: limitText(String(item?.titlePreview || item?.titleText || ""), 180),
|
||||
titleHash: item?.titleHash ?? sha256(String(item?.titlePreview || item?.titleText || "")),
|
||||
titleBytes: item?.titleBytes ?? null,
|
||||
})) : [];
|
||||
const fallbackItems = Array.isArray(value.fallbackItems)
|
||||
? value.fallbackItems.slice(0, 20).map((item) => ({
|
||||
index: item?.index ?? null,
|
||||
active: item?.active === true,
|
||||
sessionIdPrefix: item?.sessionIdPrefix ?? (item?.sessionId ? String(item.sessionId).slice(0, 12) : null),
|
||||
titlePreview: limitText(String(item?.titlePreview || item?.titleText || ""), 180),
|
||||
titleHash: item?.titleHash ?? sha256(String(item?.titlePreview || item?.titleText || "")),
|
||||
}))
|
||||
: items.filter((item) => item.fallbackTitle).slice(0, 20);
|
||||
const visibleCount = Number(value.visibleCount ?? items.length);
|
||||
const fallbackTitleCount = Number(value.fallbackTitleCount ?? fallbackItems.length);
|
||||
return {
|
||||
visibleCount: Number.isFinite(visibleCount) ? visibleCount : items.length,
|
||||
fallbackTitleCount: Number.isFinite(fallbackTitleCount) ? fallbackTitleCount : fallbackItems.length,
|
||||
fallbackTitleRatio: Number.isFinite(Number(value.fallbackTitleRatio)) ? Number(value.fallbackTitleRatio) : (items.length > 0 ? Number((fallbackItems.length / items.length).toFixed(4)) : 0),
|
||||
items,
|
||||
fallbackItems,
|
||||
valuesRedacted: true
|
||||
};
|
||||
}
|
||||
|
||||
function compactLoadingItems(items) {
|
||||
if (!Array.isArray(items)) return [];
|
||||
return items.map((item) => {
|
||||
@@ -2122,6 +2266,17 @@ function compactLoadingMetricsForOutput(value) {
|
||||
};
|
||||
}
|
||||
|
||||
function compactSessionRailTitleMetricsForOutput(value) {
|
||||
if (!value || typeof value !== "object") return null;
|
||||
return {
|
||||
summary: value.summary ?? null,
|
||||
samples: Array.isArray(value.samples) ? value.samples.slice(0, 12) : [],
|
||||
examples: Array.isArray(value.examples) ? value.examples.slice(0, 12) : [],
|
||||
timeline: Array.isArray(value.timeline) ? value.timeline.slice(-12) : [],
|
||||
valuesRedacted: true
|
||||
};
|
||||
}
|
||||
|
||||
function compactSamplePageProvenance(value) {
|
||||
if (!value || typeof value !== "object") return null;
|
||||
return {
|
||||
@@ -2189,6 +2344,8 @@ function buildFindings(samples, control, network, errors, sampleMetrics, promptN
|
||||
const loadingSummary = sampleMetrics?.loading?.summary || {};
|
||||
if (Number(loadingSummary.longestContinuousSeconds ?? 0) > 5) findings.push({ id: "page-loading-visible-over-5s", severity: "red", summary: "visible 加载中 stayed on screen longer than 5s; fix real loading latency instead of revealing incomplete content early", count: loadingSummary.overFiveSecondSegmentCount ?? 1, longestContinuousSeconds: loadingSummary.longestContinuousSeconds, segments: sampleMetrics.loading.segments.slice(0, 20), owners: sampleMetrics.loading.owners.slice(0, 20) });
|
||||
if (Number(loadingSummary.maxSimultaneousCount ?? 0) > 1) findings.push({ id: "page-loading-concurrent", severity: "info", summary: "multiple 加载中 indicators were visible in the same sampled DOM point", count: loadingSummary.concurrentLoadingSampleCount ?? 0, maxSimultaneousCount: loadingSummary.maxSimultaneousCount, owners: sampleMetrics.loading.owners.slice(0, 20) });
|
||||
const sessionRailTitleSummary = sampleMetrics?.sessionRailTitles?.summary || {};
|
||||
if (Number(sessionRailTitleSummary.majorityFallbackSampleCount ?? 0) > 0) findings.push({ id: "session-rail-title-fallback-majority", severity: "red", summary: "more than half of visible session list rows used fallback Session ses_... titles", count: sessionRailTitleSummary.majorityFallbackSampleCount, maxFallbackRatio: sessionRailTitleSummary.maxFallbackRatio, maxFallbackTitleCount: sessionRailTitleSummary.maxFallbackTitleCount, samples: sampleMetrics.sessionRailTitles.samples.slice(0, 20), examples: sampleMetrics.sessionRailTitles.examples.slice(0, 20) });
|
||||
if ((runtimeAlerts?.summary?.httpErrorCount ?? 0) > 0) findings.push({ id: "runtime-http-errors", severity: "amber", summary: "natural page requests returned HTTP error status during observation", count: runtimeAlerts.summary.httpErrorCount, groups: runtimeAlerts.networkHttpErrorsByPath.slice(0, 12) });
|
||||
if ((runtimeAlerts?.summary?.requestFailedCount ?? 0) > 0) findings.push({ id: "runtime-requestfailed", severity: "amber", summary: "browser requestfailed events were captured during observation", count: runtimeAlerts.summary.requestFailedCount, groups: runtimeAlerts.networkRequestFailedByPath.slice(0, 12) });
|
||||
if ((runtimeAlerts?.summary?.domDiagnosticSampleCount ?? 0) > 0) findings.push({ id: "runtime-dom-diagnostics", severity: "amber", summary: "diagnostic/error/warning-like text was visible in sampled DOM", count: runtimeAlerts.summary.domDiagnosticSampleCount, groupCount: runtimeAlerts.summary.domDiagnosticGroupCount ?? 0, groups: runtimeAlerts.domDiagnosticsByText.slice(0, 12), samples: runtimeAlerts.domDiagnostics.slice(0, 12) });
|
||||
@@ -3171,6 +3328,7 @@ function prioritizeFindings(findings) {
|
||||
const kindRank = (item) => {
|
||||
const id = String(item?.id ?? item?.kind ?? item?.code ?? "");
|
||||
if (id === "page-performance-slow-same-origin-api") return 0;
|
||||
if (id === "session-rail-title-fallback-majority") return 0.5;
|
||||
if (id.startsWith("turn-timing-total-elapsed")) return 1;
|
||||
if (id.startsWith("turn-timing-recent-update")) return 2;
|
||||
if (id.includes("runtime-execution") || id.includes("prompt-chat-submit-failed")) return 3;
|
||||
@@ -3219,8 +3377,11 @@ function buildSampleMetrics(samples, control) {
|
||||
messageCount: Array.isArray(sample.messages) ? sample.messages.length : 0,
|
||||
traceRowCount: Array.isArray(sample.traceRows) ? sample.traceRows.length : 0,
|
||||
loadingCount: loadings.length,
|
||||
loadingOwnerCount: loadingOwners.length,
|
||||
loadingOwners: loadingOwners.map((item) => ({ ownerKey: item.ownerKey, ownerKind: item.ownerKind, ownerLabel: item.ownerLabel, count: item.count })).slice(0, 12),
|
||||
loadingOwnerCount: loadingOwners.length,
|
||||
loadingOwners: loadingOwners.map((item) => ({ ownerKey: item.ownerKey, ownerKind: item.ownerKind, ownerLabel: item.ownerLabel, count: item.count })).slice(0, 12),
|
||||
sessionRailVisibleCount: Number(sample?.sessionRail?.visibleCount ?? 0),
|
||||
sessionRailFallbackTitleCount: Number(sample?.sessionRail?.fallbackTitleCount ?? 0),
|
||||
sessionRailFallbackTitleRatio: Number(sample?.sessionRail?.fallbackTitleRatio ?? 0),
|
||||
totalElapsedSeconds: totalElapsedValues.length > 0 ? Math.max(...totalElapsedValues) : null,
|
||||
recentUpdateSeconds: recentUpdateValues.length > 0 ? Math.max(...recentUpdateValues) : null,
|
||||
terminalSeen: terminalTexts.length > 0,
|
||||
@@ -3257,6 +3418,7 @@ function buildSampleMetrics(samples, control) {
|
||||
const withRecent = timeline.filter((item) => item.recentUpdateSeconds !== null).length;
|
||||
const diagnostics = timeline.filter((item) => item.diagnosticSeen).length;
|
||||
const loading = buildLoadingMetrics(samples, timeline);
|
||||
const sessionRailTitles = buildSessionRailTitleMetrics(samples, timeline);
|
||||
const rounds = buildRoundMetricSummaries(timeline, promptCommands, {
|
||||
nonMonotonic: turnTimingNonMonotonic,
|
||||
elapsedZeroResets: turnTimingElapsedZeroResets,
|
||||
@@ -3280,6 +3442,12 @@ function buildSampleMetrics(samples, control) {
|
||||
loadingLongestContinuousSeconds: loading.summary.longestContinuousSeconds,
|
||||
loadingCurrentContinuousSeconds: loading.summary.currentContinuousSeconds,
|
||||
loadingOverFiveSecondSegmentCount: loading.summary.overFiveSecondSegmentCount,
|
||||
sessionRailSampleCount: sessionRailTitles.summary.sampleCount,
|
||||
sessionRailVisibleSampleCount: sessionRailTitles.summary.visibleSampleCount,
|
||||
sessionRailFallbackMajoritySampleCount: sessionRailTitles.summary.majorityFallbackSampleCount,
|
||||
sessionRailFallbackMaxRatio: sessionRailTitles.summary.maxFallbackRatio,
|
||||
sessionRailFallbackMaxVisibleCount: sessionRailTitles.summary.maxVisibleCount,
|
||||
sessionRailFallbackMaxCount: sessionRailTitles.summary.maxFallbackTitleCount,
|
||||
promptSegments: Math.max(0, promptTimes.length),
|
||||
rounds: rounds.length,
|
||||
turnColumns: turnTiming.columns.length,
|
||||
@@ -3306,6 +3474,7 @@ function buildSampleMetrics(samples, control) {
|
||||
roundsWithRecentUpdateJumps: rounds.filter((item) => item.turnTimingRecentUpdateJumpCount > 0).length
|
||||
},
|
||||
loading,
|
||||
sessionRailTitles,
|
||||
rounds,
|
||||
turnColumns: turnTiming.columns,
|
||||
turnTimingTable: turnTiming.rows,
|
||||
@@ -3321,6 +3490,70 @@ function buildSampleMetrics(samples, control) {
|
||||
};
|
||||
}
|
||||
|
||||
function buildSessionRailTitleMetrics(samples, timeline) {
|
||||
const rows = [];
|
||||
const examplesByHash = new Map();
|
||||
for (let index = 0; index < (Array.isArray(samples) ? samples : []).length; index += 1) {
|
||||
const sample = samples[index];
|
||||
const rail = sample?.sessionRail && typeof sample.sessionRail === "object" ? sample.sessionRail : null;
|
||||
if (!rail) continue;
|
||||
const visibleCount = Number(rail.visibleCount ?? 0);
|
||||
const fallbackTitleCount = Number(rail.fallbackTitleCount ?? 0);
|
||||
const safeVisibleCount = Number.isFinite(visibleCount) && visibleCount > 0 ? visibleCount : 0;
|
||||
const safeFallbackTitleCount = Number.isFinite(fallbackTitleCount) && fallbackTitleCount > 0 ? fallbackTitleCount : 0;
|
||||
const fallbackTitleRatio = safeVisibleCount > 0 ? Number((safeFallbackTitleCount / safeVisibleCount).toFixed(4)) : 0;
|
||||
const fallbackItems = Array.isArray(rail.fallbackItems) ? rail.fallbackItems : [];
|
||||
for (const item of fallbackItems) {
|
||||
const hash = String(item?.titleHash || item?.titlePreview || item?.sessionIdPrefix || "").trim();
|
||||
if (!hash || examplesByHash.has(hash)) continue;
|
||||
examplesByHash.set(hash, {
|
||||
titleHash: item?.titleHash ?? null,
|
||||
titlePreview: limitText(String(item?.titlePreview || ""), 160),
|
||||
sessionIdPrefix: item?.sessionIdPrefix ?? null,
|
||||
active: item?.active === true,
|
||||
firstSeq: sample?.seq ?? null,
|
||||
firstAt: sample?.ts ?? null,
|
||||
pageRole: sample?.pageRole ?? null,
|
||||
});
|
||||
}
|
||||
rows.push({
|
||||
...ref(sample),
|
||||
promptIndex: timeline[index]?.promptIndex ?? 0,
|
||||
visibleCount: safeVisibleCount,
|
||||
fallbackTitleCount: safeFallbackTitleCount,
|
||||
fallbackTitleRatio,
|
||||
majorityFallback: safeVisibleCount > 0 && safeFallbackTitleCount > safeVisibleCount / 2,
|
||||
examples: fallbackItems.slice(0, 5).map((item) => ({
|
||||
titleHash: item?.titleHash ?? null,
|
||||
titlePreview: limitText(String(item?.titlePreview || ""), 160),
|
||||
sessionIdPrefix: item?.sessionIdPrefix ?? null,
|
||||
active: item?.active === true,
|
||||
})),
|
||||
});
|
||||
}
|
||||
const visibleRows = rows.filter((item) => item.visibleCount > 0);
|
||||
const majorityRows = rows.filter((item) => item.majorityFallback);
|
||||
const fallbackRows = rows.filter((item) => item.fallbackTitleCount > 0);
|
||||
const maxFallbackRatio = rows.length > 0 ? Math.max(...rows.map((item) => Number(item.fallbackTitleRatio) || 0)) : 0;
|
||||
const maxVisibleCount = rows.length > 0 ? Math.max(...rows.map((item) => Number(item.visibleCount) || 0)) : 0;
|
||||
const maxFallbackTitleCount = rows.length > 0 ? Math.max(...rows.map((item) => Number(item.fallbackTitleCount) || 0)) : 0;
|
||||
return {
|
||||
summary: {
|
||||
sampleCount: rows.length,
|
||||
visibleSampleCount: visibleRows.length,
|
||||
fallbackSampleCount: fallbackRows.length,
|
||||
majorityFallbackSampleCount: majorityRows.length,
|
||||
maxFallbackRatio,
|
||||
maxVisibleCount,
|
||||
maxFallbackTitleCount,
|
||||
},
|
||||
samples: majorityRows.slice(0, 80),
|
||||
examples: Array.from(examplesByHash.values()).slice(0, 80),
|
||||
timeline: rows.slice(-200),
|
||||
valuesRedacted: true
|
||||
};
|
||||
}
|
||||
|
||||
function uniqueLoadingOwners(loadings) {
|
||||
const groups = new Map();
|
||||
for (let index = 0; index < (Array.isArray(loadings) ? loadings : []).length; index += 1) {
|
||||
@@ -4392,6 +4625,8 @@ function renderMarkdown(report) {
|
||||
const metricSummary = report.sampleMetrics?.summary || {};
|
||||
const loading = report.sampleMetrics?.loading || {};
|
||||
const loadingSummary = loading.summary || {};
|
||||
const sessionRailTitles = report.sampleMetrics?.sessionRailTitles || {};
|
||||
const sessionRailTitleSummary = sessionRailTitles.summary || {};
|
||||
const alertSummary = report.runtimeAlerts?.summary || {};
|
||||
const httpAlertLines = Array.isArray(report.runtimeAlerts?.networkHttpErrorsByPath) && report.runtimeAlerts.networkHttpErrorsByPath.length > 0
|
||||
? report.runtimeAlerts.networkHttpErrorsByPath.slice(0, 40).map((item) => "- HTTP " + (item.status ?? "-") + " " + item.method + " " + item.urlPath + " count=" + item.count + " prompts=" + (item.promptIndexes?.join(",") || "-") + " first=" + (item.firstAt || "-") + " last=" + (item.lastAt || "-")).join("\n")
|
||||
@@ -4423,6 +4658,12 @@ function renderMarkdown(report) {
|
||||
const loadingTimelineLines = Array.isArray(loading.timeline) && loading.timeline.length > 0
|
||||
? loading.timeline.slice(0, 160).map((item) => "- #" + (item.seq ?? "-") + " " + (item.ts || "-") + " prompt=" + (item.promptIndex ?? "-") + " loadingCount=" + (item.loadingCount ?? 0) + " ownerCount=" + (item.ownerCount ?? 0) + " owners=" + ((Array.isArray(item.owners) ? item.owners : []).slice(0, 6).map((owner) => (owner.ownerKind || "-") + ":" + (owner.ownerLabel || "-") + "x" + (owner.count ?? 0)).join(",") || "-")).join("\n")
|
||||
: "- 未观察到“加载中”采样点。";
|
||||
const sessionRailTitleSampleLines = Array.isArray(sessionRailTitles.samples) && sessionRailTitles.samples.length > 0
|
||||
? sessionRailTitles.samples.slice(0, 80).map((item) => "- #" + (item.seq ?? "-") + " " + (item.ts || "-") + " role=" + (item.pageRole || "-") + " visible=" + (item.visibleCount ?? 0) + " fallback=" + (item.fallbackTitleCount ?? 0) + " ratio=" + (item.fallbackTitleRatio ?? 0) + " examples=" + ((Array.isArray(item.examples) ? item.examples : []).slice(0, 4).map((example) => escapeMarkdownCell(example.titlePreview || example.titleHash || "-")).join(",") || "-")).join("\n")
|
||||
: "- 未观察到超过一半 fallback 的 session 列表采样点。";
|
||||
const sessionRailTitleExampleLines = Array.isArray(sessionRailTitles.examples) && sessionRailTitles.examples.length > 0
|
||||
? sessionRailTitles.examples.slice(0, 80).map((item) => "- firstSeq=" + (item.firstSeq ?? "-") + " role=" + (item.pageRole || "-") + " active=" + String(item.active === true) + " sessionPrefix=" + (item.sessionIdPrefix || "-") + " titleHash=" + (item.titleHash || "-") + " preview=" + escapeMarkdownCell(item.titlePreview || "")).join("\n")
|
||||
: "- 无 fallback 标题示例。";
|
||||
const provenanceLines = Array.isArray(report.pageProvenance?.segments) && report.pageProvenance.segments.length > 0
|
||||
? report.pageProvenance.segments.slice(0, 40).map((item) => "- fingerprint=" + (item.assetFingerprint || "-") + " samples=" + item.sampleCount + " seq=" + (item.firstSeq ?? "-") + ".." + (item.lastSeq ?? "-") + " ts=" + (item.firstAt || "-") + ".." + (item.lastAt || "-") + " scripts=" + (item.scriptCount ?? "-") + " styles=" + (item.stylesheetCount ?? "-") + " urlPaths=" + (Array.isArray(item.urlPaths) ? item.urlPaths.slice(0, 4).join(",") : "-")).join("\n")
|
||||
: "- 无页面 provenance segment。";
|
||||
@@ -4454,6 +4695,9 @@ function renderMarkdown(report) {
|
||||
+ "- loadingLongestContinuousSeconds: " + (metricSummary.loadingLongestContinuousSeconds ?? 0) + "\n"
|
||||
+ "- loadingCurrentContinuousSeconds: " + (metricSummary.loadingCurrentContinuousSeconds ?? 0) + "\n"
|
||||
+ "- loadingOverFiveSecondSegmentCount: " + (metricSummary.loadingOverFiveSecondSegmentCount ?? 0) + "\n"
|
||||
+ "- sessionRailFallbackMajoritySampleCount: " + (metricSummary.sessionRailFallbackMajoritySampleCount ?? 0) + "\n"
|
||||
+ "- sessionRailFallbackMaxRatio: " + (metricSummary.sessionRailFallbackMaxRatio ?? 0) + "\n"
|
||||
+ "- sessionRailFallbackMaxCount: " + (metricSummary.sessionRailFallbackMaxCount ?? 0) + "\n"
|
||||
+ "- promptSegments: " + (metricSummary.promptSegments ?? 0) + "\n\n"
|
||||
+ "- turnColumns: " + (metricSummary.turnColumns ?? 0) + "\n"
|
||||
+ "- turnTimingRows: " + (metricSummary.turnTimingRows ?? 0) + "\n"
|
||||
@@ -4485,6 +4729,17 @@ function renderMarkdown(report) {
|
||||
+ "#### Loading segments\n\n" + loadingSegmentLines + "\n\n"
|
||||
+ "#### Loading owners\n\n" + loadingOwnerLines + "\n\n"
|
||||
+ "#### Loading sample timeline\n\n" + loadingTimelineLines + "\n\n"
|
||||
+ "### Session rail titles\n\n"
|
||||
+ "- sampleCount: " + (sessionRailTitleSummary.sampleCount ?? 0) + "\n"
|
||||
+ "- visibleSampleCount: " + (sessionRailTitleSummary.visibleSampleCount ?? 0) + "\n"
|
||||
+ "- fallbackSampleCount: " + (sessionRailTitleSummary.fallbackSampleCount ?? 0) + "\n"
|
||||
+ "- majorityFallbackSampleCount: " + (sessionRailTitleSummary.majorityFallbackSampleCount ?? 0) + "\n"
|
||||
+ "- maxFallbackRatio: " + (sessionRailTitleSummary.maxFallbackRatio ?? 0) + "\n"
|
||||
+ "- maxVisibleCount: " + (sessionRailTitleSummary.maxVisibleCount ?? 0) + "\n"
|
||||
+ "- maxFallbackTitleCount: " + (sessionRailTitleSummary.maxFallbackTitleCount ?? 0) + "\n"
|
||||
+ "- policy: 可见 session 列表中 'Session ses_...' fallback 标题超过一半必须报警;修复应让上游 session list projection 直接携带名称,不能靠点击详情后下游修补。\n\n"
|
||||
+ "#### Session rail fallback samples\n\n" + sessionRailTitleSampleLines + "\n\n"
|
||||
+ "#### Session rail fallback examples\n\n" + sessionRailTitleExampleLines + "\n\n"
|
||||
+ "### Page provenance\n\n"
|
||||
+ "- segmentCount: " + (report.pageProvenance?.summary?.segmentCount ?? 0) + "\n"
|
||||
+ "- controlSegmentCount: " + (report.pageProvenance?.summary?.controlSegmentCount ?? 0) + "\n\n"
|
||||
|
||||
Reference in New Issue
Block a user