feat: add web sentinel request-rate curves
This commit is contained in:
@@ -391,7 +391,8 @@ select {
|
||||
|
||||
.trend-red,
|
||||
.trend-warning,
|
||||
.memory-line {
|
||||
.memory-line,
|
||||
.request-rate-line {
|
||||
fill: none;
|
||||
stroke-width: 2.4;
|
||||
stroke-linecap: round;
|
||||
@@ -545,6 +546,42 @@ select {
|
||||
background: #327b89;
|
||||
}
|
||||
|
||||
.legend-swatch.request-rate.request-rate-line-1,
|
||||
.request-rate-line-1 {
|
||||
stroke: #c2410c;
|
||||
background: #c2410c;
|
||||
}
|
||||
|
||||
.legend-swatch.request-rate.request-rate-line-2,
|
||||
.request-rate-line-2 {
|
||||
stroke: #a16207;
|
||||
background: #a16207;
|
||||
}
|
||||
|
||||
.legend-swatch.request-rate.request-rate-line-3,
|
||||
.request-rate-line-3 {
|
||||
stroke: #7c3aed;
|
||||
background: #7c3aed;
|
||||
}
|
||||
|
||||
.legend-swatch.request-rate.request-rate-line-4,
|
||||
.request-rate-line-4 {
|
||||
stroke: #be185d;
|
||||
background: #be185d;
|
||||
}
|
||||
|
||||
.legend-swatch.request-rate.request-rate-line-5,
|
||||
.request-rate-line-5 {
|
||||
stroke: #0f766e;
|
||||
background: #0f766e;
|
||||
}
|
||||
|
||||
.legend-swatch.request-rate.request-rate-line-6,
|
||||
.request-rate-line-6 {
|
||||
stroke: #4338ca;
|
||||
background: #4338ca;
|
||||
}
|
||||
|
||||
.timeline-panel {
|
||||
display: flex;
|
||||
min-height: 0;
|
||||
@@ -1012,7 +1049,14 @@ select {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.memory-chart {
|
||||
.request-rate-chart-wrap {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.memory-chart,
|
||||
.request-rate-chart {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 192px;
|
||||
@@ -1021,6 +1065,10 @@ select {
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.request-rate-chart {
|
||||
background: #fffdfb;
|
||||
}
|
||||
|
||||
.memory-legend {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
@@ -1030,6 +1078,10 @@ select {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.request-rate-empty {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.memory-grid-line {
|
||||
stroke: #e5ecea;
|
||||
stroke-width: 1;
|
||||
|
||||
@@ -121,6 +121,19 @@ createApp({
|
||||
return Array.isArray(rows) ? rows : [];
|
||||
});
|
||||
const detailMemory = computed(() => selectedDetail.value?.memory && typeof selectedDetail.value.memory === "object" ? selectedDetail.value.memory : {});
|
||||
const detailRequestRate = computed(() => selectedDetail.value?.requestRate && typeof selectedDetail.value.requestRate === "object" ? selectedDetail.value.requestRate : {});
|
||||
const detailRequestRateSeries = computed(() => {
|
||||
const rate = detailRequestRate.value;
|
||||
const rows = [
|
||||
...tagRequestRateSeries(Array.isArray(rate.totalSeries) ? rate.totalSeries : [], "total"),
|
||||
...tagRequestRateSeries(Array.isArray(rate.pageSeries) ? rate.pageSeries : [], "page"),
|
||||
...tagRequestRateSeries(Array.isArray(rate.apiPathSeries) ? rate.apiPathSeries : [], "apiPath"),
|
||||
];
|
||||
return rows.map((series) => ({
|
||||
...series,
|
||||
points: Array.isArray(series.points) ? series.points.filter((point) => Number.isFinite(Number(point?.requestPerMinute))) : [],
|
||||
})).filter((series) => series.points.length > 0);
|
||||
});
|
||||
const detailMemorySeries = computed(() => {
|
||||
const rows = Array.isArray(detailMemory.value.pageSeries) ? detailMemory.value.pageSeries : [];
|
||||
return rows.map((series) => ({
|
||||
@@ -129,7 +142,40 @@ createApp({
|
||||
})).filter((series) => series.points.length > 0);
|
||||
});
|
||||
const detailMemoryMax = computed(() => Math.max(1, ...detailMemorySeries.value.flatMap((series) => series.points.map((point) => number(point.memoryMb)))));
|
||||
const detailMemoryDurationMax = computed(() => Math.max(1, ...detailMemorySeries.value.flatMap((series) => series.points.map((point) => number(point.elapsedMinutes)))));
|
||||
const detailRequestRateMax = computed(() => Math.max(
|
||||
1,
|
||||
number(detailRequestRate.value.totalPeakPerMinute),
|
||||
number(detailRequestRate.value.pagePeakPerMinute),
|
||||
number(detailRequestRate.value.apiPathPeakPerMinute),
|
||||
...detailRequestRateSeries.value.flatMap((series) => series.points.map((point) => number(point.requestPerMinute))),
|
||||
));
|
||||
const detailTimelineStartMs = computed(() => {
|
||||
const values = [
|
||||
...detailMemorySeries.value.flatMap((series) => series.points.map(pointTimeMs)),
|
||||
...detailRequestRateSeries.value.flatMap((series) => series.points.map(pointTimeMs)),
|
||||
].filter((value) => value !== null);
|
||||
return values.length === 0 ? null : Math.min(...values);
|
||||
});
|
||||
const detailTimelineDurationMax = computed(() => {
|
||||
const startMs = detailTimelineStartMs.value;
|
||||
const values = [
|
||||
...detailMemorySeries.value.flatMap((series) => series.points.map((point) => pointElapsedMinutes(point, startMs))),
|
||||
...detailRequestRateSeries.value.flatMap((series) => series.points.map((point) => pointElapsedMinutes(point, startMs))),
|
||||
].filter((value) => Number.isFinite(value));
|
||||
return Math.max(1, ...values);
|
||||
});
|
||||
const detailMemoryDurationMax = computed(() => detailTimelineDurationMax.value);
|
||||
const detailRequestRateYAxisTicks = computed(() => memoryTickValues(detailRequestRateMax.value).map((value) => ({
|
||||
key: `request-rate-y-${value}`,
|
||||
value,
|
||||
y: requestRateYValue(value),
|
||||
label: formatRpm(value),
|
||||
})));
|
||||
const detailRequestRatePeak = computed(() => {
|
||||
const peak = optionalNumber(detailRequestRate.value.totalPeakPerMinute);
|
||||
if (peak !== null) return peak;
|
||||
return detailRequestRateSeries.value.length > 0 ? detailRequestRateMax.value : null;
|
||||
});
|
||||
const detailMemoryYAxisTicks = computed(() => memoryTickValues(detailMemoryMax.value).map((value) => ({
|
||||
key: `memory-y-${value}`,
|
||||
value,
|
||||
@@ -409,9 +455,14 @@ createApp({
|
||||
return points.map((point) => `${memoryX(point)},${memoryY(point)}`).join(" ");
|
||||
}
|
||||
|
||||
function requestRatePolyline(series) {
|
||||
const points = Array.isArray(series?.points) ? series.points : [];
|
||||
if (points.length < 2) return "";
|
||||
return points.map((point) => `${memoryX(point)},${requestRateY(point)}`).join(" ");
|
||||
}
|
||||
|
||||
function memoryX(point) {
|
||||
const elapsed = number(point?.elapsedMinutes);
|
||||
return memoryXValue(elapsed);
|
||||
return memoryXValue(pointElapsedMinutes(point, detailTimelineStartMs.value));
|
||||
}
|
||||
|
||||
function memoryY(point) {
|
||||
@@ -419,6 +470,10 @@ createApp({
|
||||
return memoryYValue(memory);
|
||||
}
|
||||
|
||||
function requestRateY(point) {
|
||||
return requestRateYValue(number(point?.requestPerMinute));
|
||||
}
|
||||
|
||||
function memoryXValue(elapsedMinutes) {
|
||||
const ratio = clamp(number(elapsedMinutes) / detailMemoryDurationMax.value, 0, 1);
|
||||
return Math.round(memoryChartFrame.left + ratio * (memoryChartFrame.right - memoryChartFrame.left));
|
||||
@@ -429,16 +484,31 @@ createApp({
|
||||
return Math.round(memoryChartFrame.bottom - ratio * (memoryChartFrame.bottom - memoryChartFrame.top));
|
||||
}
|
||||
|
||||
function requestRateYValue(requestPerMinute) {
|
||||
const ratio = clamp(number(requestPerMinute) / detailRequestRateMax.value, 0, 1);
|
||||
return Math.round(memoryChartFrame.bottom - ratio * (memoryChartFrame.bottom - memoryChartFrame.top));
|
||||
}
|
||||
|
||||
function memoryLineClass(index) {
|
||||
return `memory-line-${(index % 6) + 1}`;
|
||||
}
|
||||
|
||||
function requestRateLineClass(index) {
|
||||
return `request-rate-line-${(index % 6) + 1}`;
|
||||
}
|
||||
|
||||
function memorySeriesLabel(series) {
|
||||
const sampleCount = Number.isFinite(Number(series?.sampleCount)) ? Number(series.sampleCount) : Array.isArray(series?.points) ? series.points.length : 0;
|
||||
const latest = Array.isArray(series?.points) && series.points.length > 0 ? series.points[series.points.length - 1] : null;
|
||||
return `${series?.label || series?.pageRole || series?.pageId || "page"} · 最新 ${formatMb(latest?.memoryMb)} · 样本 ${sampleCount}`;
|
||||
}
|
||||
|
||||
function requestRateSeriesLabel(series) {
|
||||
const sampleCount = Number.isFinite(Number(series?.bucketCount)) ? Number(series.bucketCount) : Array.isArray(series?.points) ? series.points.length : 0;
|
||||
const latest = Array.isArray(series?.points) && series.points.length > 0 ? series.points[series.points.length - 1] : null;
|
||||
return `${series?.label || series?.path || series?.key || "request"} · 最新 ${formatRpm(latest?.requestPerMinute)} · 峰值 ${formatRpm(series?.peakRequestPerMinute)} · 桶 ${sampleCount}`;
|
||||
}
|
||||
|
||||
function showTrendTooltip(dot) {
|
||||
hoveredTrendDot.value = dot;
|
||||
}
|
||||
@@ -506,6 +576,11 @@ createApp({
|
||||
timelineRuns,
|
||||
historicalCheckFindings,
|
||||
runDetailCheckFindings,
|
||||
detailRequestRate,
|
||||
detailRequestRateSeries,
|
||||
detailRequestRateMax,
|
||||
detailRequestRatePeak,
|
||||
detailRequestRateYAxisTicks,
|
||||
detailMemory,
|
||||
detailMemorySeries,
|
||||
detailMemoryMax,
|
||||
@@ -546,6 +621,9 @@ createApp({
|
||||
trendErrorCount,
|
||||
trendWarningCount,
|
||||
trendDurationMinutes,
|
||||
requestRatePolyline,
|
||||
requestRateLineClass,
|
||||
requestRateSeriesLabel,
|
||||
memoryPolyline,
|
||||
memoryLineClass,
|
||||
memorySeriesLabel,
|
||||
@@ -557,6 +635,7 @@ createApp({
|
||||
formatDuration,
|
||||
formatMinutes,
|
||||
formatMb,
|
||||
formatRpm,
|
||||
shortId,
|
||||
rootCauseText,
|
||||
findingTitle,
|
||||
@@ -867,11 +946,50 @@ createApp({
|
||||
:data-memory-page-count="detailMemorySeries.length"
|
||||
:data-memory-sample-count="detailMemory.sampleCount || 0"
|
||||
:data-memory-source="detailMemory.source || ''"
|
||||
:data-request-rate-series-count="detailRequestRateSeries.length"
|
||||
:data-request-rate-source="detailRequestRate.source || ''"
|
||||
>
|
||||
<div class="detail-card-heading">
|
||||
<h3>页面内存曲线</h3>
|
||||
<span class="tag">每个页面一条线 · 峰值 {{ formatMb(detailMemory.maxMemoryMb || detailMemoryMax) }}</span>
|
||||
<h3>请求频率 / 页面内存曲线</h3>
|
||||
<span class="tag">请求峰值 {{ formatRpm(detailRequestRatePeak) }} · 内存峰值 {{ formatMb(detailMemory.maxMemoryMb || detailMemoryMax) }}</span>
|
||||
</div>
|
||||
<div v-if="detailRequestRateSeries.length > 0" class="request-rate-chart-wrap" data-run-request-rate-chart="true">
|
||||
<svg class="request-rate-chart" viewBox="0 0 720 178" role="img" aria-label="运行详情 API 请求频率曲线,纵轴为每分钟请求数,横轴与内存曲线对齐">
|
||||
<g data-request-rate-axis-y="true">
|
||||
<g v-for="tick in detailRequestRateYAxisTicks" :key="tick.key" data-request-rate-axis-y-tick="true">
|
||||
<line class="memory-grid-line" :x1="memoryFrame.left" :x2="memoryFrame.right" :y1="tick.y" :y2="tick.y"></line>
|
||||
<text class="memory-axis-label memory-axis-label-y" :x="memoryFrame.yLabelX" :y="tick.y + 4">{{ tick.label }}</text>
|
||||
</g>
|
||||
<line class="memory-axis-line" :x1="memoryFrame.left" :x2="memoryFrame.left" :y1="memoryFrame.top" :y2="memoryFrame.bottom"></line>
|
||||
<text class="memory-axis-title memory-axis-title-y" x="18" y="78" transform="rotate(-90 18 78)">请求 rpm</text>
|
||||
</g>
|
||||
<g data-request-rate-axis-x="true">
|
||||
<line class="memory-axis-line" :x1="memoryFrame.left" :x2="memoryFrame.right" :y1="memoryFrame.bottom" :y2="memoryFrame.bottom"></line>
|
||||
<g v-for="tick in detailMemoryXAxisTicks" :key="'request-' + tick.key" data-request-rate-axis-x-tick="true">
|
||||
<line class="memory-tick-line" :x1="tick.x" :x2="tick.x" :y1="memoryFrame.top" :y2="memoryFrame.bottom + 6"></line>
|
||||
<text class="memory-axis-label memory-axis-label-x" :x="tick.x" :y="memoryFrame.xLabelY">{{ tick.label }}</text>
|
||||
</g>
|
||||
<text class="memory-axis-title memory-axis-title-x" :x="(memoryFrame.left + memoryFrame.right) / 2" :y="memoryFrame.xTitleY">运行分钟</text>
|
||||
</g>
|
||||
<polyline
|
||||
v-for="(series, index) in detailRequestRateSeries"
|
||||
:key="series.key || series.label || index"
|
||||
class="request-rate-line"
|
||||
:class="requestRateLineClass(index)"
|
||||
:points="requestRatePolyline(series)"
|
||||
></polyline>
|
||||
</svg>
|
||||
<div class="memory-legend request-rate-legend">
|
||||
<span
|
||||
v-for="(series, index) in detailRequestRateSeries"
|
||||
:key="(series.key || series.label || index) + '-request-rate-legend'"
|
||||
class="legend-item"
|
||||
>
|
||||
<span class="legend-swatch request-rate" :class="requestRateLineClass(index)"></span>{{ requestRateSeriesLabel(series) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="empty request-rate-empty">暂无 API 请求频率样本</div>
|
||||
<div v-if="detailMemorySeries.length > 0" class="memory-chart-wrap">
|
||||
<svg class="memory-chart" viewBox="0 0 720 178" role="img" aria-label="运行详情页面内存曲线,纵轴为 MB,横轴为运行分钟">
|
||||
<g data-memory-axis-y="true">
|
||||
@@ -1280,11 +1398,39 @@ function formatMb(value) {
|
||||
return `${text} MB`;
|
||||
}
|
||||
|
||||
function formatRpm(value) {
|
||||
const numberValue = optionalNumber(value);
|
||||
if (numberValue === null) return "-";
|
||||
const rounded = Math.round(numberValue * 10) / 10;
|
||||
const text = Number.isInteger(rounded) ? String(rounded) : rounded.toFixed(1).replace(/\.0$/u, "");
|
||||
return `${text} rpm`;
|
||||
}
|
||||
|
||||
function memoryTickValues(maxValue) {
|
||||
const max = Math.max(1, number(maxValue));
|
||||
return [0, max / 2, max];
|
||||
}
|
||||
|
||||
function tagRequestRateSeries(rows, scope) {
|
||||
return rows.map((series) => ({
|
||||
...series,
|
||||
scope: series?.scope || scope,
|
||||
}));
|
||||
}
|
||||
|
||||
function pointTimeMs(point) {
|
||||
const raw = point?.ts || point?.startAt;
|
||||
if (typeof raw !== "string" || raw.length === 0) return null;
|
||||
const parsed = Date.parse(raw);
|
||||
return Number.isFinite(parsed) ? parsed : null;
|
||||
}
|
||||
|
||||
function pointElapsedMinutes(point, timelineStartMs) {
|
||||
const tsMs = pointTimeMs(point);
|
||||
if (tsMs !== null && Number.isFinite(Number(timelineStartMs))) return Math.max(0, (tsMs - Number(timelineStartMs)) / 60000);
|
||||
return number(point?.elapsedMinutes);
|
||||
}
|
||||
|
||||
function runDurationText(run) {
|
||||
const timing = run?.timing || {};
|
||||
return formatMinutes(optionalNumber(run?.runDurationMinutes, run?.durationMinutes, timing.runDurationMinutes, timing.durationMinutes));
|
||||
|
||||
@@ -220,6 +220,10 @@ export interface HwlabRuntimeWebProbeAlertThresholdsSpec {
|
||||
readonly screenshotTimeoutRedCount: number;
|
||||
readonly pageErrorRedCount: number;
|
||||
readonly browserProcessSampleIntervalMs: number;
|
||||
readonly requestRateBucketMs: number;
|
||||
readonly requestRateTotalRedPerMinute: number;
|
||||
readonly requestRatePageRedPerMinute: number;
|
||||
readonly requestRateApiPathRedPerMinute: number;
|
||||
readonly browserTotalRssRedMb: number;
|
||||
readonly browserProcessRssRedMb: number;
|
||||
readonly browserRssGrowthRedMb: number;
|
||||
@@ -1332,6 +1336,10 @@ function webProbeAlertThresholdsConfig(value: unknown, path: string): HwlabRunti
|
||||
screenshotTimeoutRedCount: positiveNumberField(raw, "screenshotTimeoutRedCount", path),
|
||||
pageErrorRedCount: positiveNumberField(raw, "pageErrorRedCount", path),
|
||||
browserProcessSampleIntervalMs: positiveNumberField(raw, "browserProcessSampleIntervalMs", path),
|
||||
requestRateBucketMs: positiveNumberField(raw, "requestRateBucketMs", path),
|
||||
requestRateTotalRedPerMinute: positiveNumberField(raw, "requestRateTotalRedPerMinute", path),
|
||||
requestRatePageRedPerMinute: positiveNumberField(raw, "requestRatePageRedPerMinute", path),
|
||||
requestRateApiPathRedPerMinute: positiveNumberField(raw, "requestRateApiPathRedPerMinute", path),
|
||||
browserTotalRssRedMb: positiveNumberField(raw, "browserTotalRssRedMb", path),
|
||||
browserProcessRssRedMb: positiveNumberField(raw, "browserProcessRssRedMb", path),
|
||||
browserRssGrowthRedMb: positiveNumberField(raw, "browserRssGrowthRedMb", path),
|
||||
|
||||
@@ -54,6 +54,7 @@ const transitions = buildTransitions(samples);
|
||||
const sampleMetrics = buildSampleMetrics(samples, control);
|
||||
const pageProvenance = buildPageProvenanceReport(samples, control, manifest);
|
||||
const pagePerformance = buildPagePerformanceReport(samples, manifest);
|
||||
const requestRate = buildRequestRateReport(network);
|
||||
const promptNetwork = buildPromptNetworkReport(control, promptNetworkRows);
|
||||
const runtimeAlerts = buildRuntimeAlerts(samples, control, network, consoleEvents, errors);
|
||||
const apiDomLag = buildApiDomLagReport(samples, network);
|
||||
@@ -108,7 +109,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, apiDomLag, browserProcess)];
|
||||
const findings = [...toolFindings, ...buildProjectManagementFindings(projectManagement), ...buildFindings(samples, control, network, errors, sampleMetrics, promptNetwork, runtimeAlerts, pagePerformance, requestRate, pageProvenance, commandFailures, manifest, apiDomLag, browserProcess)];
|
||||
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, browserProcessRows, 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 }));
|
||||
@@ -128,6 +129,7 @@ const report = {
|
||||
sampleMetrics,
|
||||
pageProvenance,
|
||||
pagePerformance,
|
||||
requestRate,
|
||||
projectManagement,
|
||||
promptNetwork,
|
||||
runtimeAlerts,
|
||||
@@ -159,6 +161,7 @@ console.log(JSON.stringify({
|
||||
archiveSummary: {
|
||||
sampleMetrics: sampleMetrics.summary,
|
||||
pagePerformance: pagePerformance.summary,
|
||||
requestRate: requestRate.summary,
|
||||
projectManagement: projectManagement.summary,
|
||||
runtimeAlerts: runtimeAlerts.summary,
|
||||
apiDomLag: apiDomLag.summary,
|
||||
@@ -180,6 +183,7 @@ console.log(JSON.stringify({
|
||||
},
|
||||
pageProvenance: recentWindow.pageProvenance.summary,
|
||||
pagePerformance: recentWindow.pagePerformance.summary,
|
||||
requestRate: recentWindow.requestRate.summary,
|
||||
projectManagement: compactProjectManagementForOutput(projectManagement),
|
||||
promptNetwork: recentWindow.promptNetwork.summary,
|
||||
runtimeAlerts: recentWindow.runtimeAlerts.summary,
|
||||
@@ -309,6 +313,8 @@ console.log(JSON.stringify({
|
||||
timingSourceOfTruth: item.timingSourceOfTruth ?? null,
|
||||
timingStatus: item.timingStatus ?? null,
|
||||
})),
|
||||
requestRateCurve: compactRequestRateForOutput(recentWindow.requestRate),
|
||||
requestRatePeaks: recentWindow.requestRate.peaks.slice(0, 8),
|
||||
pagePerformanceSlowApi: recentWindow.pagePerformance.sameOriginApiByPath.filter((item) => item.isLongLivedStream !== true && Number(item.overBudgetCount ?? item.overFiveSecondCount ?? 0) > 0).slice(0, 5).map((item) => ({ path: item.path, route: item.route, sampleCount: item.sampleCount, p95Ms: item.p95Ms, maxMs: item.maxMs, overBudgetCount: item.overBudgetCount, budgetMs: item.budgetMs, overFiveSecondCount: item.overFiveSecondCount, slowSamples: item.slowSamples })),
|
||||
archivePagePerformanceSlowApi: pagePerformance.sameOriginApiByPath.filter((item) => item.isLongLivedStream !== true && Number(item.overBudgetCount ?? item.overFiveSecondCount ?? 0) > 0).slice(0, 8).map((item) => ({ path: item.path, route: item.route, sampleCount: item.sampleCount, p95Ms: item.p95Ms, maxMs: item.maxMs, overBudgetCount: item.overBudgetCount, budgetMs: item.budgetMs, overFiveSecondCount: item.overFiveSecondCount, slowSamples: item.slowSamples })),
|
||||
pagePerformancePartialApi: recentWindow.pagePerformance.sameOriginApiByPath.filter((item) => item.isLongLivedStream !== true && Number(item.partialOverBudgetCount ?? item.partialOverFiveSecondCount ?? 0) > 0).slice(0, 5).map((item) => ({ path: item.path, route: item.route, sampleCount: item.sampleCount, completeTimingSampleCount: item.completeTimingSampleCount, partialTimingSampleCount: item.partialTimingSampleCount, partialOverBudgetCount: item.partialOverBudgetCount, budgetMs: item.partialBudgetMs, partialOverFiveSecondCount: item.partialOverFiveSecondCount, partialSamples: item.partialSamples })),
|
||||
@@ -570,6 +576,10 @@ function parseAlertThresholds(value) {
|
||||
screenshotTimeoutRedCount: requiredPositiveThreshold(raw, "screenshotTimeoutRedCount"),
|
||||
pageErrorRedCount: requiredPositiveThreshold(raw, "pageErrorRedCount"),
|
||||
browserProcessSampleIntervalMs: requiredPositiveThreshold(raw, "browserProcessSampleIntervalMs"),
|
||||
requestRateBucketMs: requiredPositiveThreshold(raw, "requestRateBucketMs"),
|
||||
requestRateTotalRedPerMinute: requiredPositiveThreshold(raw, "requestRateTotalRedPerMinute"),
|
||||
requestRatePageRedPerMinute: requiredPositiveThreshold(raw, "requestRatePageRedPerMinute"),
|
||||
requestRateApiPathRedPerMinute: requiredPositiveThreshold(raw, "requestRateApiPathRedPerMinute"),
|
||||
browserTotalRssRedMb: requiredPositiveThreshold(raw, "browserTotalRssRedMb"),
|
||||
browserProcessRssRedMb: requiredPositiveThreshold(raw, "browserProcessRssRedMb"),
|
||||
browserRssGrowthRedMb: requiredPositiveThreshold(raw, "browserRssGrowthRedMb"),
|
||||
@@ -3259,7 +3269,7 @@ function promptCommandHasAuthoritativeSubmitSideEffect(control, promptRound) {
|
||||
|| Number(sideEffect.messageCountDelta ?? 0) > 0;
|
||||
}
|
||||
|
||||
function buildFindings(samples, control, network, errors, sampleMetrics, promptNetwork, runtimeAlerts, pagePerformance, pageProvenance, commandFailures = [], manifest = {}, apiDomLag = null, browserProcess = null) {
|
||||
function buildFindings(samples, control, network, errors, sampleMetrics, promptNetwork, runtimeAlerts, pagePerformance, requestRate, pageProvenance, commandFailures = [], manifest = {}, apiDomLag = null, browserProcess = 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) });
|
||||
@@ -3590,6 +3600,40 @@ function buildFindings(samples, control, network, errors, sampleMetrics, promptN
|
||||
const slowStreamOpen = longLivedStreams.filter((item) => Number(item.streamOpenOverBudgetCount ?? item.streamOpenOverFiveSecondCount ?? 0) > 0);
|
||||
if (slowStreamOpen.length > 0) findings.push({ id: "page-performance-slow-long-lived-stream-open", severity: "red", summary: "long-lived stream open latency exceeded configured YAML usability budget; stream lifetime is still reported separately", count: slowStreamOpen.length, budgetMs: alertThresholds.longLivedStreamOpenSlowMs, groups: slowStreamOpen.slice(0, 20) });
|
||||
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) });
|
||||
const requestRatePeaks = Array.isArray(requestRate?.peaks) ? requestRate.peaks : [];
|
||||
const totalRequestRatePeaks = requestRatePeaks.filter((item) => item.scope === "total" && item.overThreshold === true);
|
||||
const pageRequestRatePeaks = requestRatePeaks.filter((item) => item.scope === "page" && item.overThreshold === true);
|
||||
const apiPathRequestRatePeaks = requestRatePeaks.filter((item) => item.scope === "apiPath" && item.overThreshold === true);
|
||||
if (totalRequestRatePeaks.length > 0) findings.push({
|
||||
id: "request-rate-total-peak",
|
||||
severity: "red",
|
||||
summary: "same-origin API request rate exceeded the YAML total peak threshold; this is request storm evidence from browser network events",
|
||||
count: totalRequestRatePeaks.length,
|
||||
thresholdPerMinute: alertThresholds.requestRateTotalRedPerMinute,
|
||||
bucketMs: requestRate?.summary?.bucketMs ?? alertThresholds.requestRateBucketMs,
|
||||
peaks: totalRequestRatePeaks.slice(0, 20),
|
||||
valuesRedacted: true
|
||||
});
|
||||
if (pageRequestRatePeaks.length > 0) findings.push({
|
||||
id: "request-rate-page-peak",
|
||||
severity: "red",
|
||||
summary: "a page-level same-origin API request curve exceeded the YAML peak threshold; inspect the page and top API paths before changing probe sampling",
|
||||
count: pageRequestRatePeaks.length,
|
||||
thresholdPerMinute: alertThresholds.requestRatePageRedPerMinute,
|
||||
bucketMs: requestRate?.summary?.bucketMs ?? alertThresholds.requestRateBucketMs,
|
||||
peaks: pageRequestRatePeaks.slice(0, 20),
|
||||
valuesRedacted: true
|
||||
});
|
||||
if (apiPathRequestRatePeaks.length > 0) findings.push({
|
||||
id: "request-rate-api-path-peak",
|
||||
severity: "red",
|
||||
summary: "an API-path request curve exceeded the YAML peak threshold; this pinpoints the route most likely involved in a request storm",
|
||||
count: apiPathRequestRatePeaks.length,
|
||||
thresholdPerMinute: alertThresholds.requestRateApiPathRedPerMinute,
|
||||
bucketMs: requestRate?.summary?.bucketMs ?? alertThresholds.requestRateBucketMs,
|
||||
peaks: apiPathRequestRatePeaks.slice(0, 20),
|
||||
valuesRedacted: true
|
||||
});
|
||||
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 || "")));
|
||||
const apiDomLagSummary = effectiveApiDomLag?.summary || {};
|
||||
@@ -4395,11 +4439,12 @@ function buildRecentAnalysisWindow({ samples, control, network, consoleEvents, e
|
||||
const sampleMetrics = buildSampleMetrics(windowSamples, control);
|
||||
const pageProvenance = buildPageProvenanceReport(windowSamples, windowControl, manifest);
|
||||
const pagePerformance = buildPagePerformanceReport(windowSamples, manifest);
|
||||
const requestRate = buildRequestRateReport(windowNetwork);
|
||||
const promptNetwork = buildPromptNetworkReport(windowControl, windowNetwork);
|
||||
const runtimeAlerts = buildRuntimeAlerts(windowSamples, control, windowNetwork, windowConsole, windowErrors);
|
||||
const apiDomLag = buildApiDomLagReport(windowSamples, windowNetwork);
|
||||
const browserProcess = buildBrowserProcessReport(windowBrowserProcessRows);
|
||||
const findings = buildFindings(windowSamples, control, windowNetwork, windowErrors, sampleMetrics, promptNetwork, runtimeAlerts, pagePerformance, pageProvenance, [], {}, apiDomLag, browserProcess);
|
||||
const findings = buildFindings(windowSamples, control, windowNetwork, windowErrors, sampleMetrics, promptNetwork, runtimeAlerts, pagePerformance, requestRate, pageProvenance, [], {}, apiDomLag, browserProcess);
|
||||
return {
|
||||
summary: {
|
||||
name: "recent-5m",
|
||||
@@ -4417,6 +4462,7 @@ function buildRecentAnalysisWindow({ samples, control, network, consoleEvents, e
|
||||
sampleMetrics,
|
||||
pageProvenance,
|
||||
pagePerformance,
|
||||
requestRate,
|
||||
promptNetwork,
|
||||
runtimeAlerts,
|
||||
apiDomLag,
|
||||
@@ -4822,6 +4868,280 @@ function percentile(sortedValues, percentileValue) {
|
||||
return Math.round(sortedValues[lower] * (1 - weight) + sortedValues[upper] * weight);
|
||||
}
|
||||
|
||||
function buildRequestRateReport(network) {
|
||||
const bucketMs = Math.max(1000, Number(alertThresholds.requestRateBucketMs || 0));
|
||||
const naturalApiRequests = (Array.isArray(network) ? network : [])
|
||||
.filter((item) => item?.observerInitiated !== true && item?.type === "request")
|
||||
.map((item) => requestRateEvent(item))
|
||||
.filter((item) => item !== null)
|
||||
.sort((a, b) => a.tsMs - b.tsMs);
|
||||
if (naturalApiRequests.length === 0) {
|
||||
return {
|
||||
summary: {
|
||||
bucketMs,
|
||||
bucketSeconds: Number((bucketMs / 1000).toFixed(3)),
|
||||
requestCount: 0,
|
||||
bucketCount: 0,
|
||||
pageCount: 0,
|
||||
apiPathCount: 0,
|
||||
totalRedPerMinute: alertThresholds.requestRateTotalRedPerMinute,
|
||||
pageRedPerMinute: alertThresholds.requestRatePageRedPerMinute,
|
||||
apiPathRedPerMinute: alertThresholds.requestRateApiPathRedPerMinute,
|
||||
totalPeakPerMinute: 0,
|
||||
pagePeakPerMinute: 0,
|
||||
apiPathPeakPerMinute: 0,
|
||||
overThresholdPeakCount: 0,
|
||||
valuesRedacted: true,
|
||||
},
|
||||
buckets: [],
|
||||
pageCurves: [],
|
||||
apiPathCurves: [],
|
||||
peaks: [],
|
||||
valuesRedacted: true,
|
||||
};
|
||||
}
|
||||
const firstBucketMs = Math.floor(naturalApiRequests[0].tsMs / bucketMs) * bucketMs;
|
||||
const lastBucketMs = Math.floor(naturalApiRequests[naturalApiRequests.length - 1].tsMs / bucketMs) * bucketMs;
|
||||
const makeBucket = (bucketStartMs, extra = {}) => ({
|
||||
bucketStartMs,
|
||||
bucketEndMs: bucketStartMs + bucketMs,
|
||||
startAt: new Date(bucketStartMs).toISOString(),
|
||||
endAt: new Date(bucketStartMs + bucketMs).toISOString(),
|
||||
count: 0,
|
||||
requestPerMinute: 0,
|
||||
...extra,
|
||||
valuesRedacted: true,
|
||||
});
|
||||
const totalBuckets = new Map();
|
||||
const pageBuckets = new Map();
|
||||
const apiPathBuckets = new Map();
|
||||
const pageTotals = new Map();
|
||||
const apiPathTotals = new Map();
|
||||
const allBucketStarts = [];
|
||||
for (let bucketStartMs = firstBucketMs; bucketStartMs <= lastBucketMs; bucketStartMs += bucketMs) {
|
||||
totalBuckets.set(bucketStartMs, makeBucket(bucketStartMs));
|
||||
allBucketStarts.push(bucketStartMs);
|
||||
}
|
||||
for (const item of naturalApiRequests) {
|
||||
const bucketStartMs = Math.floor(item.tsMs / bucketMs) * bucketMs;
|
||||
const totalBucket = totalBuckets.get(bucketStartMs) || makeBucket(bucketStartMs);
|
||||
totalBucket.count += 1;
|
||||
totalBuckets.set(bucketStartMs, totalBucket);
|
||||
|
||||
const pageKey = item.pageKey;
|
||||
const pageMapKey = pageKey + "|" + bucketStartMs;
|
||||
const pageBucket = pageBuckets.get(pageMapKey) || makeBucket(bucketStartMs, {
|
||||
pageKey,
|
||||
pageRole: item.pageRole,
|
||||
pageId: item.pageId,
|
||||
pageEpoch: item.pageEpoch,
|
||||
path: item.framePath,
|
||||
});
|
||||
pageBucket.count += 1;
|
||||
pageBuckets.set(pageMapKey, pageBucket);
|
||||
const pageTotal = pageTotals.get(pageKey) || { pageKey, pageRole: item.pageRole, pageId: item.pageId, pageEpoch: item.pageEpoch, path: item.framePath, count: 0 };
|
||||
pageTotal.count += 1;
|
||||
pageTotals.set(pageKey, pageTotal);
|
||||
|
||||
const apiKey = item.method + " " + item.apiPath;
|
||||
const apiMapKey = apiKey + "|" + bucketStartMs;
|
||||
const apiBucket = apiPathBuckets.get(apiMapKey) || makeBucket(bucketStartMs, {
|
||||
apiKey,
|
||||
method: item.method,
|
||||
path: item.apiPath,
|
||||
pageKey,
|
||||
pageRole: item.pageRole,
|
||||
pageId: item.pageId,
|
||||
pageEpoch: item.pageEpoch,
|
||||
});
|
||||
apiBucket.count += 1;
|
||||
apiPathBuckets.set(apiMapKey, apiBucket);
|
||||
const apiTotal = apiPathTotals.get(apiKey) || { apiKey, method: item.method, path: item.apiPath, count: 0 };
|
||||
apiTotal.count += 1;
|
||||
apiPathTotals.set(apiKey, apiTotal);
|
||||
}
|
||||
const finalizeBucket = (bucket) => ({
|
||||
...bucket,
|
||||
requestPerMinute: Number((Number(bucket.count || 0) * 60000 / bucketMs).toFixed(2)),
|
||||
});
|
||||
const buckets = Array.from(totalBuckets.values()).map(finalizeBucket);
|
||||
const pageBucketRows = Array.from(pageBuckets.values()).map(finalizeBucket);
|
||||
const apiPathBucketRows = Array.from(apiPathBuckets.values()).map(finalizeBucket);
|
||||
const peakForRows = (rows, thresholdPerMinute, scope, extra = {}) => rows
|
||||
.filter((row) => Number(row.requestPerMinute ?? 0) >= Number(thresholdPerMinute || Infinity))
|
||||
.map((row) => ({
|
||||
scope,
|
||||
thresholdPerMinute,
|
||||
overThreshold: true,
|
||||
bucketMs,
|
||||
bucketStartMs: row.bucketStartMs,
|
||||
bucketEndMs: row.bucketEndMs,
|
||||
startAt: row.startAt,
|
||||
endAt: row.endAt,
|
||||
count: row.count,
|
||||
requestPerMinute: row.requestPerMinute,
|
||||
...extra(row),
|
||||
valuesRedacted: true,
|
||||
}));
|
||||
const totalPeaks = peakForRows(buckets, alertThresholds.requestRateTotalRedPerMinute, "total", () => ({}));
|
||||
const pagePeaks = peakForRows(pageBucketRows, alertThresholds.requestRatePageRedPerMinute, "page", (row) => ({
|
||||
pageKey: row.pageKey,
|
||||
pageRole: row.pageRole,
|
||||
pageId: row.pageId,
|
||||
pageEpoch: row.pageEpoch,
|
||||
path: row.path,
|
||||
}));
|
||||
const apiPathPeaks = peakForRows(apiPathBucketRows, alertThresholds.requestRateApiPathRedPerMinute, "apiPath", (row) => ({
|
||||
apiKey: row.apiKey,
|
||||
method: row.method,
|
||||
path: row.path,
|
||||
pageKey: row.pageKey,
|
||||
pageRole: row.pageRole,
|
||||
pageId: row.pageId,
|
||||
pageEpoch: row.pageEpoch,
|
||||
}));
|
||||
const pageCurves = Array.from(pageTotals.values())
|
||||
.map((page) => {
|
||||
const rows = pageBucketRows
|
||||
.filter((row) => row.pageKey === page.pageKey)
|
||||
.sort((a, b) => a.bucketStartMs - b.bucketStartMs);
|
||||
const peak = maxByNumber(rows, (row) => row.requestPerMinute);
|
||||
return {
|
||||
...page,
|
||||
bucketCount: rows.length,
|
||||
peakRequestPerMinute: peak?.requestPerMinute ?? 0,
|
||||
peakBucket: peak ?? null,
|
||||
buckets: rows.slice(-60),
|
||||
valuesRedacted: true,
|
||||
};
|
||||
})
|
||||
.sort((a, b) => Number(b.peakRequestPerMinute ?? 0) - Number(a.peakRequestPerMinute ?? 0) || Number(b.count ?? 0) - Number(a.count ?? 0));
|
||||
const apiPathCurves = Array.from(apiPathTotals.values())
|
||||
.map((api) => {
|
||||
const rows = apiPathBucketRows
|
||||
.filter((row) => row.apiKey === api.apiKey)
|
||||
.sort((a, b) => a.bucketStartMs - b.bucketStartMs);
|
||||
const peak = maxByNumber(rows, (row) => row.requestPerMinute);
|
||||
return {
|
||||
...api,
|
||||
bucketCount: rows.length,
|
||||
peakRequestPerMinute: peak?.requestPerMinute ?? 0,
|
||||
peakBucket: peak ?? null,
|
||||
buckets: rows.slice(-60),
|
||||
valuesRedacted: true,
|
||||
};
|
||||
})
|
||||
.sort((a, b) => Number(b.peakRequestPerMinute ?? 0) - Number(a.peakRequestPerMinute ?? 0) || Number(b.count ?? 0) - Number(a.count ?? 0));
|
||||
const totalPeak = maxByNumber(buckets, (row) => row.requestPerMinute);
|
||||
const pagePeak = pageCurves[0] ?? null;
|
||||
const apiPathPeak = apiPathCurves[0] ?? null;
|
||||
const peaks = [...totalPeaks, ...pagePeaks, ...apiPathPeaks]
|
||||
.sort((a, b) => Number(b.requestPerMinute ?? 0) - Number(a.requestPerMinute ?? 0) || String(a.scope).localeCompare(String(b.scope)));
|
||||
return {
|
||||
summary: {
|
||||
bucketMs,
|
||||
bucketSeconds: Number((bucketMs / 1000).toFixed(3)),
|
||||
requestCount: naturalApiRequests.length,
|
||||
bucketCount: buckets.length,
|
||||
pageCount: pageCurves.length,
|
||||
apiPathCount: apiPathCurves.length,
|
||||
firstAt: new Date(firstBucketMs).toISOString(),
|
||||
lastAt: new Date(lastBucketMs + bucketMs).toISOString(),
|
||||
totalRedPerMinute: alertThresholds.requestRateTotalRedPerMinute,
|
||||
pageRedPerMinute: alertThresholds.requestRatePageRedPerMinute,
|
||||
apiPathRedPerMinute: alertThresholds.requestRateApiPathRedPerMinute,
|
||||
totalPeakPerMinute: totalPeak?.requestPerMinute ?? 0,
|
||||
totalPeakCount: totalPeak?.count ?? 0,
|
||||
totalPeakAt: totalPeak?.startAt ?? null,
|
||||
pagePeakPerMinute: pagePeak?.peakRequestPerMinute ?? 0,
|
||||
pagePeakKey: pagePeak?.pageKey ?? null,
|
||||
pagePeakPath: pagePeak?.path ?? null,
|
||||
apiPathPeakPerMinute: apiPathPeak?.peakRequestPerMinute ?? 0,
|
||||
apiPathPeakKey: apiPathPeak?.apiKey ?? null,
|
||||
overThresholdPeakCount: peaks.length,
|
||||
valuesRedacted: true,
|
||||
},
|
||||
buckets: buckets.slice(-120),
|
||||
pageCurves: pageCurves.slice(0, 20),
|
||||
apiPathCurves: apiPathCurves.slice(0, 40),
|
||||
peaks: peaks.slice(0, 80),
|
||||
valuesRedacted: true,
|
||||
};
|
||||
}
|
||||
|
||||
function requestRateEvent(item) {
|
||||
const tsMs = Date.parse(String(item?.ts || ""));
|
||||
const apiPath = normalizeApiRatePath(urlPath(item?.url));
|
||||
if (!Number.isFinite(tsMs) || !isApiLikePath(apiPath)) return null;
|
||||
const framePath = normalizeRoutePath(urlPath(item?.frameUrl || item?.url));
|
||||
const pageRole = String(item?.pageRole || "unknown");
|
||||
const pageId = String(item?.pageId || "unknown");
|
||||
const pageEpoch = Number.isFinite(Number(item?.pageEpoch)) ? Number(item.pageEpoch) : null;
|
||||
return {
|
||||
ts: item.ts,
|
||||
tsMs,
|
||||
method: String(item?.method || "GET").toUpperCase(),
|
||||
apiPath,
|
||||
framePath,
|
||||
pageRole,
|
||||
pageId,
|
||||
pageEpoch,
|
||||
pageKey: [pageRole, pageId, pageEpoch ?? "-", framePath || "-"].join("|"),
|
||||
valuesRedacted: true,
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeApiRatePath(value) {
|
||||
return normalizeRoutePath(value)
|
||||
.replace(/\/sessions\/[^/?#]+/gu, "/sessions/:id")
|
||||
.replace(/\/turns\/[^/?#]+/gu, "/turns/:id")
|
||||
.replace(/\/traces\/[^/?#]+/gu, "/traces/:id")
|
||||
.replace(/\/messages\/[^/?#]+/gu, "/messages/:id")
|
||||
.replace(/\/files\/[^/?#]+/gu, "/files/:id")
|
||||
.replace(/\/tasks\/[^/?#]+/gu, "/tasks/:id")
|
||||
.replace(/\/runs\/[^/?#]+/gu, "/runs/:id")
|
||||
.replace(/\/[0-9a-f]{8,}(?=\/|$)/giu, "/:id")
|
||||
.replace(/\/[A-Za-z0-9_-]{24,}(?=\/|$)/gu, "/:id");
|
||||
}
|
||||
|
||||
function normalizeRoutePath(value) {
|
||||
const raw = String(value || "");
|
||||
if (!raw || raw === "-") return "-";
|
||||
return raw.replace(/\/+/gu, "/").replace(/\/$/u, "") || "/";
|
||||
}
|
||||
|
||||
function compactRequestRateForOutput(report) {
|
||||
if (!report || typeof report !== "object") return null;
|
||||
return {
|
||||
summary: report.summary ?? null,
|
||||
buckets: Array.isArray(report.buckets) ? report.buckets.slice(-12) : [],
|
||||
pageCurves: Array.isArray(report.pageCurves) ? report.pageCurves.slice(0, 8).map((item) => ({
|
||||
pageKey: item.pageKey ?? null,
|
||||
pageRole: item.pageRole ?? null,
|
||||
pageId: item.pageId ?? null,
|
||||
pageEpoch: item.pageEpoch ?? null,
|
||||
path: item.path ?? null,
|
||||
count: item.count ?? null,
|
||||
peakRequestPerMinute: item.peakRequestPerMinute ?? null,
|
||||
peakBucket: item.peakBucket ?? null,
|
||||
buckets: Array.isArray(item.buckets) ? item.buckets.slice(-12) : [],
|
||||
valuesRedacted: true,
|
||||
})) : [],
|
||||
apiPathCurves: Array.isArray(report.apiPathCurves) ? report.apiPathCurves.slice(0, 12).map((item) => ({
|
||||
apiKey: item.apiKey ?? null,
|
||||
method: item.method ?? null,
|
||||
path: item.path ?? null,
|
||||
count: item.count ?? null,
|
||||
peakRequestPerMinute: item.peakRequestPerMinute ?? null,
|
||||
peakBucket: item.peakBucket ?? null,
|
||||
buckets: Array.isArray(item.buckets) ? item.buckets.slice(-12) : [],
|
||||
valuesRedacted: true,
|
||||
})) : [],
|
||||
peaks: Array.isArray(report.peaks) ? report.peaks.slice(0, 12) : [],
|
||||
valuesRedacted: true,
|
||||
};
|
||||
}
|
||||
|
||||
function buildPromptNetworkReport(control, network) {
|
||||
const promptsById = new Map();
|
||||
for (const item of control) {
|
||||
|
||||
@@ -5616,6 +5616,10 @@ function parseAlertThresholds(value) {
|
||||
screenshotTimeoutRedCount: requiredPositiveThreshold(raw, "screenshotTimeoutRedCount"),
|
||||
pageErrorRedCount: requiredPositiveThreshold(raw, "pageErrorRedCount"),
|
||||
browserProcessSampleIntervalMs: requiredPositiveThreshold(raw, "browserProcessSampleIntervalMs"),
|
||||
requestRateBucketMs: requiredPositiveThreshold(raw, "requestRateBucketMs"),
|
||||
requestRateTotalRedPerMinute: requiredPositiveThreshold(raw, "requestRateTotalRedPerMinute"),
|
||||
requestRatePageRedPerMinute: requiredPositiveThreshold(raw, "requestRatePageRedPerMinute"),
|
||||
requestRateApiPathRedPerMinute: requiredPositiveThreshold(raw, "requestRateApiPathRedPerMinute"),
|
||||
browserTotalRssRedMb: requiredPositiveThreshold(raw, "browserTotalRssRedMb"),
|
||||
browserProcessRssRedMb: requiredPositiveThreshold(raw, "browserProcessRssRedMb"),
|
||||
browserRssGrowthRedMb: requiredPositiveThreshold(raw, "browserRssGrowthRedMb"),
|
||||
|
||||
@@ -951,6 +951,7 @@ function dashboardRunDetail(config: WebProbeSentinelServiceConfig, db: Database,
|
||||
run: dashboardRunSummary(config, db, row),
|
||||
summary: record(stored.summary),
|
||||
memory: dashboardRunMemoryDetail(config, row, stored),
|
||||
requestRate: dashboardRunRequestRateDetail(config, row, stored),
|
||||
findings,
|
||||
viewsAvailable: Object.keys(views),
|
||||
artifacts: {
|
||||
@@ -1099,6 +1100,37 @@ function dashboardRunMemoryDetail(config: WebProbeSentinelServiceConfig, row: Re
|
||||
};
|
||||
}
|
||||
|
||||
function dashboardRunRequestRateDetail(config: WebProbeSentinelServiceConfig, row: Record<string, unknown>, stored: Record<string, unknown>): Record<string, unknown> {
|
||||
const options = dashboardDetailRequestRateOptions(config);
|
||||
const storedRequestRate = compactDashboardRequestRate(record(record(record(stored.summary).analysis).requestRate), options, "recorded-analysis-summary");
|
||||
if (storedRequestRate.ok === true && hasDashboardRequestRateSeries(storedRequestRate)) return storedRequestRate;
|
||||
const fromArtifacts = readDashboardRequestRateFromAnalysisReport(config, row, options);
|
||||
if (fromArtifacts.ok === true) return fromArtifacts;
|
||||
if (storedRequestRate.ok === true) return storedRequestRate;
|
||||
return {
|
||||
ok: false,
|
||||
source: "unavailable",
|
||||
unit: "rpm",
|
||||
metric: "same-origin-api-request-rate",
|
||||
requestCount: 0,
|
||||
bucketCount: 0,
|
||||
pageCount: 0,
|
||||
apiPathCount: 0,
|
||||
totalSeries: [],
|
||||
pageSeries: [],
|
||||
apiPathSeries: [],
|
||||
peaks: [],
|
||||
reason: fromArtifacts.reason ?? storedRequestRate.reason ?? "request-rate-samples-missing",
|
||||
valuesRedacted: true,
|
||||
};
|
||||
}
|
||||
|
||||
function hasDashboardRequestRateSeries(value: Record<string, unknown>): boolean {
|
||||
return arrayRecords(value.totalSeries).length > 0
|
||||
|| arrayRecords(value.pageSeries).length > 0
|
||||
|| arrayRecords(value.apiPathSeries).length > 0;
|
||||
}
|
||||
|
||||
function dashboardDetailMemoryOptions(config: WebProbeSentinelServiceConfig): Record<string, number> {
|
||||
const detailMemory = recordTarget(valueAtPath(config.reportViews, "detailMemory"));
|
||||
return {
|
||||
@@ -1107,6 +1139,17 @@ function dashboardDetailMemoryOptions(config: WebProbeSentinelServiceConfig): Re
|
||||
};
|
||||
}
|
||||
|
||||
function dashboardDetailRequestRateOptions(config: WebProbeSentinelServiceConfig): Record<string, number> {
|
||||
const detailRequestRate = recordTarget(valueAtPath(config.reportViews, "detailRequestRate"));
|
||||
return {
|
||||
maxTotalBuckets: Math.max(1, Math.floor(numberAt(detailRequestRate, "maxTotalBuckets"))),
|
||||
maxPageSeries: Math.max(1, Math.floor(numberAt(detailRequestRate, "maxPageSeries"))),
|
||||
maxApiPathSeries: Math.max(1, Math.floor(numberAt(detailRequestRate, "maxApiPathSeries"))),
|
||||
maxBucketsPerSeries: Math.max(1, Math.floor(numberAt(detailRequestRate, "maxBucketsPerSeries"))),
|
||||
maxPeaks: Math.max(1, Math.floor(numberAt(detailRequestRate, "maxPeaks"))),
|
||||
};
|
||||
}
|
||||
|
||||
function readDashboardMemoryFromStateDir(config: WebProbeSentinelServiceConfig, row: Record<string, unknown>, options: Record<string, number>): Record<string, unknown> {
|
||||
const stateDir = stringOrNull(row.state_dir) ?? stringOrNull(row.stateDir);
|
||||
if (stateDir === null) return { ok: false, source: "artifact-browser-process-jsonl", reason: "state-dir-missing", pageSeries: [], valuesRedacted: true };
|
||||
@@ -1141,6 +1184,160 @@ function compactDashboardMemory(value: Record<string, unknown>, options: Record<
|
||||
return { ok: false, source, reason: "recorded-page-series-missing", pageSeries: [], valuesRedacted: true };
|
||||
}
|
||||
|
||||
function readDashboardRequestRateFromAnalysisReport(config: WebProbeSentinelServiceConfig, row: Record<string, unknown>, options: Record<string, number>): Record<string, unknown> {
|
||||
const stateDir = stringOrNull(row.state_dir) ?? stringOrNull(row.stateDir);
|
||||
if (stateDir === null) return { ok: false, source: "analysis-report-json", reason: "state-dir-missing", totalSeries: [], pageSeries: [], apiPathSeries: [], valuesRedacted: true };
|
||||
if (!isSafeDashboardStateDir(stateDir)) return { ok: false, source: "analysis-report-json", reason: "unsafe-state-dir", totalSeries: [], pageSeries: [], apiPathSeries: [], valuesRedacted: true };
|
||||
const reportJsonPath = join(config.stateRoot, stateDir, "analysis", "report.json");
|
||||
const jsonBuffer = readFileBuffer(reportJsonPath);
|
||||
if (jsonBuffer === null) return { ok: false, source: "analysis-report-json", reason: "analysis-report-json-missing", totalSeries: [], pageSeries: [], apiPathSeries: [], valuesRedacted: true };
|
||||
try {
|
||||
const report = record(JSON.parse(jsonBuffer.toString("utf8")) as unknown);
|
||||
return compactDashboardRequestRate(record(report.requestRate), options, "analysis-report-json");
|
||||
} catch (error) {
|
||||
return {
|
||||
ok: false,
|
||||
source: "analysis-report-json",
|
||||
reason: "analysis-report-json-invalid",
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
totalSeries: [],
|
||||
pageSeries: [],
|
||||
apiPathSeries: [],
|
||||
valuesRedacted: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function compactDashboardRequestRate(value: Record<string, unknown>, options: Record<string, number>, source: string): Record<string, unknown> {
|
||||
const summary = requestRateSummary(record(value.summary).requestCount === undefined ? value : record(value.summary));
|
||||
const totalSeries = finalizeDashboardRequestRateSeries([{
|
||||
key: "total",
|
||||
label: "全部 API 请求",
|
||||
count: summary.requestCount,
|
||||
peakRequestPerMinute: summary.totalPeakPerMinute,
|
||||
buckets: arrayRecords(value.buckets),
|
||||
}], numberOr(options.maxTotalBuckets, 1), 1, "total");
|
||||
const pageSeries = finalizeDashboardRequestRateSeries(arrayRecords(value.pageCurves), numberOr(options.maxBucketsPerSeries, 1), numberOr(options.maxPageSeries, 1), "page");
|
||||
const apiPathSeries = finalizeDashboardRequestRateSeries(arrayRecords(value.apiPathCurves), numberOr(options.maxBucketsPerSeries, 1), numberOr(options.maxApiPathSeries, 1), "apiPath");
|
||||
const peaks = compactDashboardRequestRatePeaks(arrayRecords(value.peaks), numberOr(options.maxPeaks, 1));
|
||||
const hasRequestRate = summary.requestCount !== null || totalSeries.length > 0 || pageSeries.length > 0 || apiPathSeries.length > 0 || peaks.length > 0;
|
||||
if (!hasRequestRate) {
|
||||
return { ok: false, source, reason: "request-rate-report-missing", totalSeries: [], pageSeries: [], apiPathSeries: [], peaks: [], valuesRedacted: true };
|
||||
}
|
||||
return {
|
||||
ok: true,
|
||||
source,
|
||||
unit: "rpm",
|
||||
metric: "same-origin-api-request-rate",
|
||||
...summary,
|
||||
totalSeries,
|
||||
pageSeries,
|
||||
apiPathSeries,
|
||||
peaks,
|
||||
valuesRedacted: true,
|
||||
};
|
||||
}
|
||||
|
||||
function requestRateSummary(value: Record<string, unknown>): Record<string, unknown> {
|
||||
return {
|
||||
bucketMs: numberOrNull(value.bucketMs),
|
||||
bucketSeconds: numberOrNull(value.bucketSeconds),
|
||||
requestCount: numberOrNull(value.requestCount),
|
||||
bucketCount: numberOrNull(value.bucketCount),
|
||||
pageCount: numberOrNull(value.pageCount),
|
||||
apiPathCount: numberOrNull(value.apiPathCount),
|
||||
firstAt: stringOrNull(value.firstAt),
|
||||
lastAt: stringOrNull(value.lastAt),
|
||||
totalRedPerMinute: numberOrNull(value.totalRedPerMinute),
|
||||
pageRedPerMinute: numberOrNull(value.pageRedPerMinute),
|
||||
apiPathRedPerMinute: numberOrNull(value.apiPathRedPerMinute),
|
||||
totalPeakPerMinute: numberOrNull(value.totalPeakPerMinute),
|
||||
totalPeakCount: numberOrNull(value.totalPeakCount),
|
||||
totalPeakAt: stringOrNull(value.totalPeakAt),
|
||||
pagePeakPerMinute: numberOrNull(value.pagePeakPerMinute),
|
||||
pagePeakKey: stringOrNull(value.pagePeakKey),
|
||||
pagePeakPath: stringOrNull(value.pagePeakPath),
|
||||
apiPathPeakPerMinute: numberOrNull(value.apiPathPeakPerMinute),
|
||||
apiPathPeakKey: stringOrNull(value.apiPathPeakKey),
|
||||
overThresholdPeakCount: numberOrNull(value.overThresholdPeakCount),
|
||||
};
|
||||
}
|
||||
|
||||
function finalizeDashboardRequestRateSeries(rawSeries: readonly Record<string, unknown>[], maxBucketsPerSeries: number, maxSeries: number, scope: string): Record<string, unknown>[] {
|
||||
return rawSeries
|
||||
.map((item) => {
|
||||
const points = arrayRecords(item.buckets)
|
||||
.map((bucket) => ({
|
||||
ts: stringOrNull(bucket.startAt),
|
||||
startAt: stringOrNull(bucket.startAt),
|
||||
endAt: stringOrNull(bucket.endAt),
|
||||
bucketStartMs: numberOrNull(bucket.bucketStartMs),
|
||||
bucketEndMs: numberOrNull(bucket.bucketEndMs),
|
||||
count: numberOrNull(bucket.count),
|
||||
requestPerMinute: numberOrNull(bucket.requestPerMinute),
|
||||
valuesRedacted: true,
|
||||
}))
|
||||
.filter((point) => point.ts !== null && point.requestPerMinute !== null);
|
||||
const thinned = thinDashboardPoints(points, maxBucketsPerSeries);
|
||||
const peakRequestPerMinute = numberOrNull(item.peakRequestPerMinute) ?? maxNumber(thinned.map((point) => numberOrNull(point.requestPerMinute))) ?? numberOrNull(item.totalPeakPerMinute) ?? 0;
|
||||
const count = numberOrNull(item.count) ?? numberOrNull(item.requestCount) ?? thinned.reduce((sum, point) => sum + numberOr(point.count, 0), 0);
|
||||
return {
|
||||
key: requestRateSeriesKey(item, scope),
|
||||
scope,
|
||||
label: requestRateSeriesLabel(item, scope),
|
||||
method: stringOrNull(item.method),
|
||||
path: stringOrNull(item.path),
|
||||
pageRole: stringOrNull(item.pageRole),
|
||||
pageId: stringOrNull(item.pageId),
|
||||
pageEpoch: numberOrNull(item.pageEpoch),
|
||||
count,
|
||||
bucketCount: numberOrNull(item.bucketCount) ?? thinned.length,
|
||||
peakRequestPerMinute,
|
||||
peakAt: stringOrNull(record(item.peakBucket).startAt),
|
||||
points: thinned,
|
||||
valuesRedacted: true,
|
||||
};
|
||||
})
|
||||
.filter((item) => arrayRecords(item.points).length > 0)
|
||||
.sort((a, b) => numberOr(b.peakRequestPerMinute, 0) - numberOr(a.peakRequestPerMinute, 0) || numberOr(b.count, 0) - numberOr(a.count, 0))
|
||||
.slice(0, maxSeries);
|
||||
}
|
||||
|
||||
function compactDashboardRequestRatePeaks(rawPeaks: readonly Record<string, unknown>[], maxPeaks: number): Record<string, unknown>[] {
|
||||
return rawPeaks.slice(0, maxPeaks).map((item) => ({
|
||||
scope: stringOrNull(item.scope),
|
||||
thresholdPerMinute: numberOrNull(item.thresholdPerMinute),
|
||||
overThreshold: item.overThreshold === true,
|
||||
bucketMs: numberOrNull(item.bucketMs),
|
||||
startAt: stringOrNull(item.startAt),
|
||||
endAt: stringOrNull(item.endAt),
|
||||
count: numberOrNull(item.count),
|
||||
requestPerMinute: numberOrNull(item.requestPerMinute),
|
||||
method: stringOrNull(item.method),
|
||||
path: stringOrNull(item.path),
|
||||
apiKey: stringOrNull(item.apiKey),
|
||||
pageKey: stringOrNull(item.pageKey),
|
||||
pageRole: stringOrNull(item.pageRole),
|
||||
pageId: stringOrNull(item.pageId),
|
||||
pageEpoch: numberOrNull(item.pageEpoch),
|
||||
valuesRedacted: true,
|
||||
}));
|
||||
}
|
||||
|
||||
function requestRateSeriesKey(item: Record<string, unknown>, scope: string): string {
|
||||
const fallback = [stringOrNull(item.method), stringOrNull(item.path)].filter((part) => part !== null).join(" ");
|
||||
return stringOrNull(item.key)
|
||||
?? stringOrNull(item.apiKey)
|
||||
?? stringOrNull(item.pageKey)
|
||||
?? (fallback.length > 0 ? fallback : scope);
|
||||
}
|
||||
|
||||
function requestRateSeriesLabel(item: Record<string, unknown>, scope: string): string {
|
||||
if (scope === "total") return "全部 API 请求";
|
||||
if (scope === "apiPath") return (stringOrNull(item.apiKey) ?? [stringOrNull(item.method), stringOrNull(item.path)].filter((part) => part !== null).join(" ")) || "API";
|
||||
return stringOrNull(item.path) ?? stringOrNull(item.pageKey) ?? stringOrNull(item.pageRole) ?? "page";
|
||||
}
|
||||
|
||||
function buildDashboardMemoryDetail(rows: readonly Record<string, unknown>[], options: Record<string, number>, source: string): Record<string, unknown> {
|
||||
const seriesByKey = new Map<string, Record<string, unknown>>();
|
||||
let firstTsMs: number | null = null;
|
||||
|
||||
@@ -116,6 +116,16 @@ export function renderWebObserveAnalyzeTable(value: Record<string, unknown>): st
|
||||
const runtimeAlerts = record(analysis?.runtimeAlerts);
|
||||
const pagePerformance = record(analysis?.pagePerformance);
|
||||
const pagePerformanceSummary = record(pagePerformance?.summary);
|
||||
const nonEmptyRecord = (item: unknown): Record<string, unknown> | null => {
|
||||
const valueRecord = record(item);
|
||||
return Object.keys(valueRecord).length > 0 ? valueRecord : null;
|
||||
};
|
||||
const requestRateSummarySource = nonEmptyRecord(analysis?.requestRate);
|
||||
const requestRateCurve = nonEmptyRecord(analysis?.requestRateCurve) ?? (requestRateSummarySource?.summary ? requestRateSummarySource : null);
|
||||
const requestRateSummary = nonEmptyRecord(requestRateCurve?.summary) ?? requestRateSummarySource;
|
||||
const requestRatePeaks = webObserveArray(analysis?.requestRatePeaks ?? requestRateCurve?.peaks ?? requestRateSummarySource?.peaks).map(record).filter((item): item is Record<string, unknown> => item !== null).slice(0, 8);
|
||||
const requestRatePageCurves = webObserveArray(requestRateCurve?.pageCurves ?? requestRateSummarySource?.pageCurves).map(record).filter((item): item is Record<string, unknown> => item !== null).slice(0, 8);
|
||||
const requestRateApiPathCurves = webObserveArray(requestRateCurve?.apiPathCurves ?? requestRateSummarySource?.apiPathCurves).map(record).filter((item): item is Record<string, unknown> => item !== null).slice(0, 8);
|
||||
const projectManagement = record(analysis?.projectManagement) ?? record(value.projectManagement);
|
||||
const projectManagementSummary = record(projectManagement?.summary) ?? projectManagement;
|
||||
const projectManagementCommandsSource = Array.isArray(projectManagement?.launchCommands) && projectManagement.launchCommands.length > 0
|
||||
@@ -558,6 +568,41 @@ export function renderWebObserveAnalyzeTable(value: Record<string, unknown>): st
|
||||
webObserveText(promptNetwork?.promptSegments),
|
||||
]]),
|
||||
"",
|
||||
"Request rate:",
|
||||
webObserveTable(["BUCKET", "REQUESTS", "TOTAL_PEAK", "PAGE_PEAK", "API_PEAK", "OVER_PEAKS", "THRESHOLDS"], [[
|
||||
requestRateSummary?.bucketSeconds ? `${webObserveText(requestRateSummary.bucketSeconds)}s` : webObserveText(requestRateSummary?.bucketMs),
|
||||
webObserveText(requestRateSummary?.requestCount),
|
||||
webObserveText(requestRateSummary?.totalPeakPerMinute),
|
||||
webObserveText(requestRateSummary?.pagePeakPerMinute),
|
||||
webObserveText(requestRateSummary?.apiPathPeakPerMinute),
|
||||
webObserveText(requestRateSummary?.overThresholdPeakCount),
|
||||
`total=${webObserveText(requestRateSummary?.totalRedPerMinute)} page=${webObserveText(requestRateSummary?.pageRedPerMinute)} api=${webObserveText(requestRateSummary?.apiPathRedPerMinute)}`,
|
||||
]]),
|
||||
requestRatePeaks.length === 0
|
||||
? "Request rate peaks:\n-"
|
||||
: webObserveTable(["SCOPE", "RPM", "COUNT", "START", "PAGE", "API"], requestRatePeaks.map((item) => [
|
||||
webObserveText(item.scope),
|
||||
webObserveText(item.requestPerMinute),
|
||||
webObserveText(item.count),
|
||||
webObserveShort(webObserveText(item.startAt), 24),
|
||||
webObserveShort(webObserveText(item.path ?? item.pageKey), 48),
|
||||
webObserveShort(webObserveText(item.apiKey), 64),
|
||||
])),
|
||||
"Request page curves:",
|
||||
webObserveTable(["PAGE", "COUNT", "PEAK_RPM", "PEAK_AT"], requestRatePageCurves.length > 0 ? requestRatePageCurves.map((item) => [
|
||||
webObserveShort(webObserveText(item.path ?? item.pageKey), 56),
|
||||
webObserveText(item.count),
|
||||
webObserveText(item.peakRequestPerMinute),
|
||||
webObserveShort(webObserveText(record(item.peakBucket)?.startAt), 24),
|
||||
]) : [["-", "-", "-", "-"]]),
|
||||
"Request API curves:",
|
||||
webObserveTable(["API", "COUNT", "PEAK_RPM", "PEAK_AT"], requestRateApiPathCurves.length > 0 ? requestRateApiPathCurves.map((item) => [
|
||||
webObserveShort(webObserveText(item.apiKey ?? item.path), 72),
|
||||
webObserveText(item.count),
|
||||
webObserveText(item.peakRequestPerMinute),
|
||||
webObserveShort(webObserveText(record(item.peakBucket)?.startAt), 24),
|
||||
]) : [["-", "-", "-", "-"]]),
|
||||
"",
|
||||
"Runner errors:",
|
||||
webObserveTable(["TS", "TYPE", "RETRY", "EXH", "ATTEMPTS", "LAST_KIND", "READY", "MESSAGE"], runnerErrors.length > 0 ? runnerErrors.map((item) => [
|
||||
webObserveShort(webObserveText(item.ts), 24),
|
||||
|
||||
@@ -2252,6 +2252,10 @@ export function runNodeWebProbeObserveAnalyze(options: NodeWebProbeObserveOption
|
||||
"const slimProjectManagement = (value) => { const v = objectOrNull(value); if (!v) return null; const s = objectOrNull(v.summary) || v; return { summary: { enabled: s.enabled === true, projectSampleCount: s.projectSampleCount ?? null, mdtodoSampleCount: s.mdtodoSampleCount ?? null, latestPageKind: clip(s.latestPageKind, 48), latestPath: clip(s.latestPath, 96), latestSourceCount: s.latestSourceCount ?? null, latestFileCount: s.latestFileCount ?? null, latestTaskCount: s.latestTaskCount ?? null, latestSelectedTaskRefHash: clip(s.latestSelectedTaskRefHash, 80), launchCommandCount: s.launchCommandCount ?? null, launchSuccessCount: s.launchSuccessCount ?? null, launchFailureCount: s.launchFailureCount ?? null, launchWithOtelTraceHeaderCount: s.launchWithOtelTraceHeaderCount ?? null, projectApiResponseCount: s.projectApiResponseCount ?? null, projectApiFailureCount: s.projectApiFailureCount ?? null, projectApiSlowPathCount: s.projectApiSlowPathCount ?? null, slowApiBudgetMs: s.slowApiBudgetMs ?? null }, commands: takeTail(v.commands, 8).map((item) => { const row = objectOrNull(item) || {}; return { ts: row.ts ?? null, phase: clip(row.phase, 16), type: clip(row.type, 32), commandId: clip(row.commandId, 80), launchStatus: row.launchStatus ?? null, sessionId: clip(row.sessionId, 80), workbenchUrl: clip(row.workbenchUrl, 120), otelTraceId: clip(row.otelTraceId, 32), selectedTaskRefHash: clip(row.selectedTaskRefHash, 80) }; }), samples: takeTail(v.samples, 8).map((item) => { const row = objectOrNull(item) || {}; return { seq: row.seq ?? null, ts: row.ts ?? null, pageRole: clip(row.pageRole, 24), path: clip(row.path, 96), pageKind: clip(row.pageKind, 48), sourceCount: row.sourceCount ?? null, fileCount: row.fileCount ?? null, taskCount: row.taskCount ?? null, selectedTaskRefHash: clip(row.selectedTaskRefHash, 80), launchButtonEnabled: row.launchButtonEnabled === true, workbenchLinkCount: row.workbenchLinkCount ?? null }; }), projectApiByPath: takeHead(v.projectApiByPath, 8).map(slimNetworkGroup), valuesRedacted: true }; };",
|
||||
"const slimDomGroup = (item) => { const v = objectOrNull(item) || {}; return { count: v.count ?? null, firstAt: v.firstAt ?? null, lastAt: v.lastAt ?? null, text: clip(v.text ?? v.preview, 180) }; };",
|
||||
"const slimNetworkGroup = (item) => { const v = objectOrNull(item) || {}; return { count: v.count ?? null, method: clip(v.method, 12), status: v.status ?? null, path: clip(v.path ?? v.urlPath, 96), firstAt: v.firstAt ?? null, lastAt: v.lastAt ?? null, promptIndexes: Array.isArray(v.promptIndexes) ? v.promptIndexes.slice(0, 6) : [], failureKinds: Array.isArray(v.failureKinds) ? v.failureKinds.slice(0, 4).map((x) => clip(x, 48)) : [] }; };",
|
||||
"const slimRequestBucket = (item) => { const v = objectOrNull(item) || {}; return { startAt: v.startAt ?? null, endAt: v.endAt ?? null, count: v.count ?? null, requestPerMinute: v.requestPerMinute ?? null, pageKey: clip(v.pageKey, 96), path: clip(v.path, 96), apiKey: clip(v.apiKey, 120), valuesRedacted: true }; };",
|
||||
"const slimRequestCurve = (item, isApi = false) => { const v = objectOrNull(item) || {}; return { pageKey: clip(v.pageKey, 96), pageRole: clip(v.pageRole, 24), pageId: clip(v.pageId, 32), pageEpoch: v.pageEpoch ?? null, apiKey: clip(v.apiKey, 120), method: clip(v.method, 12), path: clip(v.path, isApi ? 120 : 96), count: v.count ?? null, bucketCount: v.bucketCount ?? null, peakRequestPerMinute: v.peakRequestPerMinute ?? null, peakBucket: slimRequestBucket(v.peakBucket), buckets: takeTail(v.buckets, 12).map(slimRequestBucket), valuesRedacted: true }; };",
|
||||
"const slimRequestPeak = (item) => { const v = objectOrNull(item) || {}; return { scope: clip(v.scope, 16), thresholdPerMinute: v.thresholdPerMinute ?? null, bucketMs: v.bucketMs ?? null, startAt: v.startAt ?? null, endAt: v.endAt ?? null, count: v.count ?? null, requestPerMinute: v.requestPerMinute ?? null, pageKey: clip(v.pageKey, 96), path: clip(v.path, 96), apiKey: clip(v.apiKey, 120), valuesRedacted: true }; };",
|
||||
"const slimRequestRateCurve = (value) => { const v = objectOrNull(value); if (!v) return null; return { summary: objectOrNull(v.summary) || v, buckets: takeTail(v.buckets, 12).map(slimRequestBucket), pageCurves: takeHead(v.pageCurves, 8).map((item) => slimRequestCurve(item, false)), apiPathCurves: takeHead(v.apiPathCurves, 12).map((item) => slimRequestCurve(item, true)), peaks: takeHead(v.peaks, 12).map(slimRequestPeak), valuesRedacted: true }; };",
|
||||
"const slimDomSample = (item) => { const v = objectOrNull(item) || {}; return { seq: v.seq ?? null, ts: v.ts ?? null, source: clip(v.source, 32), diagnosticCode: clip(v.diagnosticCode, 48), traceId: clip(v.traceId, 64), httpStatus: v.httpStatus ?? null, idleSeconds: v.idleSeconds ?? null, waitingFor: clip(v.waitingFor, 48), lastEventLabel: clip(v.lastEventLabel, 80), text: clip(v.text ?? v.preview, 180) }; };",
|
||||
"const slimConsoleGroup = (item) => { const v = objectOrNull(item) || {}; return { count: v.count ?? null, type: clip(v.type, 24), status: v.status ?? null, path: clip(v.path ?? v.urlPath, 96), lastAt: v.lastAt ?? v.firstAt ?? null, firstAt: v.firstAt ?? null, traceIds: Array.isArray(v.traceIds) ? v.traceIds.slice(0, 3).map((x) => clip(x, 64)) : [] }; };",
|
||||
"const slimConsoleSample = (item) => { const v = objectOrNull(item) || {}; return { ts: v.ts ?? null, type: clip(v.type, 24), status: v.status ?? null, path: clip(v.path ?? v.urlPath, 96), traceId: clip(v.traceId, 64), text: clip(v.text ?? v.preview, 180) }; };",
|
||||
@@ -2319,6 +2323,10 @@ export function runNodeWebProbeObserveAnalyze(options: NodeWebProbeObserveOption
|
||||
"const srcPromptNetwork = objectOrNull(source?.promptNetwork);",
|
||||
"const promptNetwork = srcPromptNetwork ? { promptSegments: srcPromptNetwork.promptSegments ?? null } : null;",
|
||||
"const projectManagement = slimProjectManagement(source?.projectManagement || fullSource?.projectManagement);",
|
||||
"const sourceRequestRate = objectOrNull(source?.requestRate);",
|
||||
"const requestRateCurve = slimRequestRateCurve(source?.requestRateCurve) || slimRequestRateCurve(sourceRequestRate) || slimRequestRateCurve(fullRecentWindow?.requestRate) || slimRequestRateCurve(fullSource?.requestRate);",
|
||||
"const requestRateSummary = objectOrNull(requestRateCurve?.summary) || objectOrNull(sourceRequestRate?.summary) || sourceRequestRate || null;",
|
||||
"const requestRatePeaks = takeHead(firstNonEmptyArray(source?.requestRatePeaks, requestRateCurve?.peaks, sourceRequestRate?.peaks, fullRecentWindow?.requestRate?.peaks, fullSource?.requestRate?.peaks), 12).map(slimRequestPeak);",
|
||||
"const runnerErrorsFromJsonl = readJsonlTail(reportJsonPath.replace(/\\/analysis\\/report\\.json$/u, '/errors.jsonl'), 8).filter((item) => item?.type === 'runner-error').map(slimRunnerErrorFromJsonl);",
|
||||
"const compact = source ? {",
|
||||
" ok: analyzerExit === 0,",
|
||||
@@ -2331,6 +2339,9 @@ export function runNodeWebProbeObserveAnalyze(options: NodeWebProbeObserveOption
|
||||
" pagePerformance,",
|
||||
" projectManagement,",
|
||||
" promptNetwork,",
|
||||
" requestRate: requestRateSummary,",
|
||||
" requestRateCurve,",
|
||||
" requestRatePeaks,",
|
||||
" pagePerformanceSlowApi: takeHead(sourceSlowApi, 4).map(slimSlowApi),",
|
||||
" archivePagePerformanceSlowApi: takeHead(archiveSlowApi, 8).map(slimSlowApi),",
|
||||
" pagePerformanceSseStreams: takeHead(sourceSseStreams, 4).map((item) => ({ path: item?.path ?? item?.route ?? null, route: item?.route ?? null, sampleCount: item?.sampleCount ?? null, streamOpenSampleCount: item?.streamOpenSampleCount ?? null, streamOpenP95Ms: item?.streamOpenP95Ms ?? null, streamOpenMaxMs: item?.streamOpenMaxMs ?? null, streamOpenBudgetMs: item?.streamOpenBudgetMs ?? null, streamOpenOverBudgetCount: item?.streamOpenOverBudgetCount ?? null, streamOpenOverFiveSecondCount: item?.streamOpenOverFiveSecondCount ?? null, streamLifetimeOverFiveSecondCount: item?.streamLifetimeOverFiveSecondCount ?? null })),",
|
||||
@@ -2418,6 +2429,9 @@ export function runNodeWebProbeObserveAnalyze(options: NodeWebProbeObserveOption
|
||||
" } : null,",
|
||||
" runtimeAlerts: compact.runtimeAlerts ?? null,",
|
||||
" pagePerformance: compact.pagePerformance ?? null,",
|
||||
" requestRate: compact.requestRate ?? null,",
|
||||
" requestRateCurve: compact.requestRateCurve ? { summary: compact.requestRateCurve.summary ?? null, buckets: Array.isArray(compact.requestRateCurve.buckets) ? compact.requestRateCurve.buckets.slice(-6) : [], pageCurves: Array.isArray(compact.requestRateCurve.pageCurves) ? compact.requestRateCurve.pageCurves.slice(0, 4).map((item) => ({ pageKey: item.pageKey ?? null, path: item.path ?? null, count: item.count ?? null, peakRequestPerMinute: item.peakRequestPerMinute ?? null, peakBucket: item.peakBucket ?? null })) : [], apiPathCurves: Array.isArray(compact.requestRateCurve.apiPathCurves) ? compact.requestRateCurve.apiPathCurves.slice(0, 6).map((item) => ({ apiKey: item.apiKey ?? null, path: item.path ?? null, count: item.count ?? null, peakRequestPerMinute: item.peakRequestPerMinute ?? null, peakBucket: item.peakBucket ?? null })) : [], valuesRedacted: true } : null,",
|
||||
" requestRatePeaks: Array.isArray(compact.requestRatePeaks) ? compact.requestRatePeaks.slice(0, 6) : [],",
|
||||
" projectManagement: compact.projectManagement ?? null,",
|
||||
" promptNetwork: compact.promptNetwork ?? null,",
|
||||
" toolFindings: Array.isArray(compact.toolFindings) ? compact.toolFindings.slice(0, 8) : [],",
|
||||
@@ -2482,6 +2496,9 @@ export function runNodeWebProbeObserveAnalyze(options: NodeWebProbeObserveOption
|
||||
" } : null,",
|
||||
" runtimeAlerts: compact.runtimeAlerts ?? null,",
|
||||
" pagePerformance: compact.pagePerformance ?? null,",
|
||||
" requestRate: compact.requestRate ?? null,",
|
||||
" requestRateCurve: compact.requestRateCurve ? { summary: compact.requestRateCurve.summary ?? null, pageCurves: Array.isArray(compact.requestRateCurve.pageCurves) ? compact.requestRateCurve.pageCurves.slice(0, 3).map((item) => ({ pageKey: item.pageKey ?? null, path: item.path ?? null, count: item.count ?? null, peakRequestPerMinute: item.peakRequestPerMinute ?? null })) : [], apiPathCurves: Array.isArray(compact.requestRateCurve.apiPathCurves) ? compact.requestRateCurve.apiPathCurves.slice(0, 4).map((item) => ({ apiKey: item.apiKey ?? null, path: item.path ?? null, count: item.count ?? null, peakRequestPerMinute: item.peakRequestPerMinute ?? null })) : [], valuesRedacted: true } : null,",
|
||||
" requestRatePeaks: Array.isArray(compact.requestRatePeaks) ? compact.requestRatePeaks.slice(0, 4) : [],",
|
||||
" projectManagement: compact.projectManagement ? { summary: compact.projectManagement.summary ?? compact.projectManagement, samples: Array.isArray(compact.projectManagement.samples) ? compact.projectManagement.samples.slice(-4) : [], commands: Array.isArray(compact.projectManagement.commands) ? compact.projectManagement.commands.slice(-4) : [], launchCommands: Array.isArray(compact.projectManagement.launchCommands) ? compact.projectManagement.launchCommands.slice(-4) : [], projectApiByPath: Array.isArray(compact.projectManagement.projectApiByPath) ? compact.projectManagement.projectApiByPath.slice(0, 4) : [], slowProjectApiPerformance: Array.isArray(compact.projectManagement.slowProjectApiPerformance) ? compact.projectManagement.slowProjectApiPerformance.slice(0, 4) : [], valuesRedacted: true } : null,",
|
||||
" toolFindings: Array.isArray(compact.toolFindings) ? compact.toolFindings.slice(0, 8) : [],",
|
||||
" commandState: compact.commandState ?? null,",
|
||||
@@ -2523,6 +2540,8 @@ export function runNodeWebProbeObserveAnalyze(options: NodeWebProbeObserveOption
|
||||
" } : null,",
|
||||
" runtimeAlerts: compact.runtimeAlerts ?? null,",
|
||||
" pagePerformance: compact.pagePerformance ?? null,",
|
||||
" requestRate: compact.requestRate ?? null,",
|
||||
" requestRatePeaks: Array.isArray(compact.requestRatePeaks) ? compact.requestRatePeaks.slice(0, 3) : [],",
|
||||
" projectManagement: compact.projectManagement ? { summary: compact.projectManagement.summary ?? compact.projectManagement, samples: Array.isArray(compact.projectManagement.samples) ? compact.projectManagement.samples.slice(-3) : [], commands: Array.isArray(compact.projectManagement.commands) ? compact.projectManagement.commands.slice(-3) : [], launchCommands: Array.isArray(compact.projectManagement.launchCommands) ? compact.projectManagement.launchCommands.slice(-3) : [], projectApiByPath: Array.isArray(compact.projectManagement.projectApiByPath) ? compact.projectManagement.projectApiByPath.slice(0, 3) : [], slowProjectApiPerformance: Array.isArray(compact.projectManagement.slowProjectApiPerformance) ? compact.projectManagement.slowProjectApiPerformance.slice(0, 2) : [], valuesRedacted: true } : null,",
|
||||
" toolFindings: Array.isArray(compact.toolFindings) ? compact.toolFindings.slice(0, 8) : [],",
|
||||
" commandState: compact.commandState ?? null,",
|
||||
@@ -2547,7 +2566,7 @@ export function runNodeWebProbeObserveAnalyze(options: NodeWebProbeObserveOption
|
||||
" output = compactOutput(ultratiny);",
|
||||
" }",
|
||||
" if (Buffer.byteLength(output, 'utf8') > compactStdoutLimitBytes) {",
|
||||
" output = compactOutput({ ok: compact.ok, counts: compact.counts ?? null, jsonlScope: compact.jsonlScope ?? null, analysisWindow: compact.analysisWindow ?? null, archiveSummary: compact.archiveSummary ? { redFindingCount: compact.archiveSummary.redFindingCount ?? null, findingCount: compact.archiveSummary.findingCount ?? null, sampleCount: compact.archiveSummary.sampleMetrics?.sampleCount ?? null, slowPathCount: compact.archiveSummary.pagePerformance?.slowPathCount ?? null } : null, projectManagement: compact.projectManagement ? { summary: compact.projectManagement.summary ?? compact.projectManagement, samples: Array.isArray(compact.projectManagement.samples) ? compact.projectManagement.samples.slice(-2) : [], commands: Array.isArray(compact.projectManagement.commands) ? compact.projectManagement.commands.slice(-2) : [], launchCommands: Array.isArray(compact.projectManagement.launchCommands) ? compact.projectManagement.launchCommands.slice(-2) : [], projectApiByPath: Array.isArray(compact.projectManagement.projectApiByPath) ? compact.projectManagement.projectApiByPath.slice(0, 2) : [], valuesRedacted: true } : null, toolFindings: Array.isArray(compact.toolFindings) ? compact.toolFindings.slice(0, 8) : [], commandState: compact.commandState ? { pendingCount: compact.commandState.pendingCount ?? null, processingCount: compact.commandState.processingCount ?? null, abandonedCount: compact.commandState.abandonedCount ?? null, failedCount: compact.commandState.failedCount ?? null } : null, findings: Array.isArray(compact.findings) ? compact.findings.slice(0, 4).map((item) => ({ kind: item.kind ?? item.code ?? null, severity: item.severity ?? item.level ?? null, count: item.count ?? item.sampleCount ?? null, timingSourceOfTruth: item.timingSourceOfTruth ?? null, timingStatus: item.timingStatus ?? null, timingAlert: item.timingAlert === true, summary: clip(item.summary ?? item.message, 120) })) : [], archiveRedFindings: Array.isArray(compact.archiveRedFindings) ? compact.archiveRedFindings.slice(0, 4).map((item) => ({ kind: item.kind ?? item.code ?? null, severity: item.severity ?? item.level ?? null, count: item.count ?? item.sampleCount ?? null, timingSourceOfTruth: item.timingSourceOfTruth ?? null, timingStatus: item.timingStatus ?? null, timingAlert: item.timingAlert === true, summary: clip(item.summary ?? item.message, 120) })) : [], commandFailures: Array.isArray(compact.commandFailures) ? compact.commandFailures.slice(-3).map((item) => ({ ts: item.ts ?? null, type: item.type ?? null, commandId: item.commandId ?? null, durationMs: item.durationMs ?? null, sampleSeq: item.sampleSeq ?? null, beforePath: item.beforePath ?? null, afterPath: item.afterPath ?? null, message: clip(item.message ?? item.failureKind ?? item.name, 120) })) : [], archivePagePerformanceSlowApi: Array.isArray(compact.archivePagePerformanceSlowApi) ? compact.archivePagePerformanceSlowApi.slice(0, 4).map((item) => ({ path: item.path ?? item.route ?? null, sampleCount: item.sampleCount ?? null, p95Ms: item.p95Ms ?? null, maxMs: item.maxMs ?? null, overFiveSecondCount: item.overFiveSecondCount ?? null })) : [], reportJsonPath: compact.reportJsonPath ?? reportJsonPath, reportJsonSha256: compact.reportJsonSha256 ?? sha256(reportJsonPath), reportMdPath: compact.reportMdPath ?? reportMdPath, reportMdSha256: compact.reportMdSha256 ?? sha256(reportMdPath), analyzer: { ...(compact.analyzer ?? {}), compactStdoutLimited: true, ultratiny: true, hardFallback: true, compactStdoutLimitBytes }, valuesRedacted: true });",
|
||||
" output = compactOutput({ ok: compact.ok, counts: compact.counts ?? null, jsonlScope: compact.jsonlScope ?? null, analysisWindow: compact.analysisWindow ?? null, archiveSummary: compact.archiveSummary ? { redFindingCount: compact.archiveSummary.redFindingCount ?? null, findingCount: compact.archiveSummary.findingCount ?? null, sampleCount: compact.archiveSummary.sampleMetrics?.sampleCount ?? null, slowPathCount: compact.archiveSummary.pagePerformance?.slowPathCount ?? null } : null, requestRate: compact.requestRate ?? null, requestRateCurve: compact.requestRateCurve ? { summary: compact.requestRateCurve.summary ?? null, pageCurves: Array.isArray(compact.requestRateCurve.pageCurves) ? compact.requestRateCurve.pageCurves.slice(0, 2).map((item) => ({ path: item.path ?? item.pageKey ?? null, count: item.count ?? null, peakRequestPerMinute: item.peakRequestPerMinute ?? null, peakBucket: item.peakBucket ?? null })) : [], apiPathCurves: Array.isArray(compact.requestRateCurve.apiPathCurves) ? compact.requestRateCurve.apiPathCurves.slice(0, 3).map((item) => ({ apiKey: item.apiKey ?? item.path ?? null, count: item.count ?? null, peakRequestPerMinute: item.peakRequestPerMinute ?? null, peakBucket: item.peakBucket ?? null })) : [], valuesRedacted: true } : null, requestRatePeaks: Array.isArray(compact.requestRatePeaks) ? compact.requestRatePeaks.slice(0, 3) : [], projectManagement: compact.projectManagement ? { summary: compact.projectManagement.summary ?? compact.projectManagement, samples: Array.isArray(compact.projectManagement.samples) ? compact.projectManagement.samples.slice(-2) : [], commands: Array.isArray(compact.projectManagement.commands) ? compact.projectManagement.commands.slice(-2) : [], launchCommands: Array.isArray(compact.projectManagement.launchCommands) ? compact.projectManagement.launchCommands.slice(-2) : [], projectApiByPath: Array.isArray(compact.projectManagement.projectApiByPath) ? compact.projectManagement.projectApiByPath.slice(0, 2) : [], valuesRedacted: true } : null, toolFindings: Array.isArray(compact.toolFindings) ? compact.toolFindings.slice(0, 8) : [], commandState: compact.commandState ? { pendingCount: compact.commandState.pendingCount ?? null, processingCount: compact.commandState.processingCount ?? null, abandonedCount: compact.commandState.abandonedCount ?? null, failedCount: compact.commandState.failedCount ?? null } : null, findings: Array.isArray(compact.findings) ? compact.findings.slice(0, 4).map((item) => ({ kind: item.kind ?? item.code ?? null, severity: item.severity ?? item.level ?? null, count: item.count ?? item.sampleCount ?? null, timingSourceOfTruth: item.timingSourceOfTruth ?? null, timingStatus: item.timingStatus ?? null, timingAlert: item.timingAlert === true, summary: clip(item.summary ?? item.message, 120) })) : [], archiveRedFindings: Array.isArray(compact.archiveRedFindings) ? compact.archiveRedFindings.slice(0, 4).map((item) => ({ kind: item.kind ?? item.code ?? null, severity: item.severity ?? item.level ?? null, count: item.count ?? item.sampleCount ?? null, timingSourceOfTruth: item.timingSourceOfTruth ?? null, timingStatus: item.timingStatus ?? null, timingAlert: item.timingAlert === true, summary: clip(item.summary ?? item.message, 120) })) : [], commandFailures: Array.isArray(compact.commandFailures) ? compact.commandFailures.slice(-3).map((item) => ({ ts: item.ts ?? null, type: item.type ?? null, commandId: item.commandId ?? null, durationMs: item.durationMs ?? null, sampleSeq: item.sampleSeq ?? null, beforePath: item.beforePath ?? null, afterPath: item.afterPath ?? null, message: clip(item.message ?? item.failureKind ?? item.name, 120) })) : [], archivePagePerformanceSlowApi: Array.isArray(compact.archivePagePerformanceSlowApi) ? compact.archivePagePerformanceSlowApi.slice(0, 4).map((item) => ({ path: item.path ?? item.route ?? null, sampleCount: item.sampleCount ?? null, p95Ms: item.p95Ms ?? null, maxMs: item.maxMs ?? null, overFiveSecondCount: item.overFiveSecondCount ?? null })) : [], reportJsonPath: compact.reportJsonPath ?? reportJsonPath, reportJsonSha256: compact.reportJsonSha256 ?? sha256(reportJsonPath), reportMdPath: compact.reportMdPath ?? reportMdPath, reportMdSha256: compact.reportMdSha256 ?? sha256(reportMdPath), analyzer: { ...(compact.analyzer ?? {}), compactStdoutLimited: true, ultratiny: true, hardFallback: true, compactStdoutLimitBytes }, valuesRedacted: true });",
|
||||
" }",
|
||||
" }",
|
||||
"}",
|
||||
@@ -2654,6 +2673,8 @@ function compactWebObserveAnalyzePayloadForRaw(payload: Record<string, unknown>,
|
||||
function compactWebObserveAnalyzeAnalysisForRaw(analysis: Record<string, unknown>): Record<string, unknown> {
|
||||
const counts = recordValue(analysis.counts);
|
||||
const archiveSummary = recordValue(analysis.archiveSummary);
|
||||
const requestRate = recordValue(analysis.requestRate);
|
||||
const requestRateCurve = recordValue(analysis.requestRateCurve);
|
||||
const findings = arrayRecordsValue(analysis.findings).slice(0, 16).map(compactWebObserveAnalyzeFindingForRaw);
|
||||
const archiveRedFindings = arrayRecordsValue(analysis.archiveRedFindings ?? archiveSummary.redFindings).slice(0, 16).map(compactWebObserveAnalyzeFindingForRaw);
|
||||
return {
|
||||
@@ -2671,6 +2692,9 @@ function compactWebObserveAnalyzeAnalysisForRaw(analysis: Record<string, unknown
|
||||
valuesRedacted: true,
|
||||
},
|
||||
runtimeAlerts: compactWebObserveAnalyzeAlertSummaryForRaw(analysis.runtimeAlerts),
|
||||
requestRate: Object.keys(requestRate).length > 0 ? requestRate : recordValue(requestRateCurve.summary),
|
||||
requestRateCurve: compactWebObserveAnalyzeRequestRateCurveForRaw(requestRateCurve),
|
||||
requestRatePeaks: arrayRecordsValue(analysis.requestRatePeaks ?? requestRateCurve.peaks ?? requestRate.peaks).slice(0, 8),
|
||||
pagePerformanceSlowApi: arrayRecordsValue(analysis.pagePerformanceSlowApi ?? analysis.archivePagePerformanceSlowApi).slice(0, 8).map((item) => ({
|
||||
path: stringOrNullValue(item.path ?? item.route),
|
||||
route: stringOrNullValue(item.route ?? item.path),
|
||||
@@ -2693,6 +2717,58 @@ function compactWebObserveAnalyzeAnalysisForRaw(analysis: Record<string, unknown
|
||||
};
|
||||
}
|
||||
|
||||
function compactWebObserveAnalyzeRequestRateCurveForRaw(value: Record<string, unknown>): Record<string, unknown> | null {
|
||||
const summary = recordValue(value.summary);
|
||||
const hasCurve = Object.keys(summary).length > 0
|
||||
|| arrayRecordsValue(value.buckets).length > 0
|
||||
|| arrayRecordsValue(value.pageCurves).length > 0
|
||||
|| arrayRecordsValue(value.apiPathCurves).length > 0
|
||||
|| arrayRecordsValue(value.peaks).length > 0;
|
||||
if (!hasCurve) return null;
|
||||
return {
|
||||
buckets: arrayRecordsValue(value.buckets).slice(-4).map(compactWebObserveAnalyzeRequestRateBucketForRaw),
|
||||
pageCurves: arrayRecordsValue(value.pageCurves).slice(0, 8).map(compactWebObserveAnalyzeRequestRateSeriesForRaw),
|
||||
apiPathCurves: arrayRecordsValue(value.apiPathCurves).slice(0, 12).map(compactWebObserveAnalyzeRequestRateSeriesForRaw),
|
||||
peaks: arrayRecordsValue(value.peaks).slice(0, 8),
|
||||
valuesRedacted: true,
|
||||
};
|
||||
}
|
||||
|
||||
function compactWebObserveAnalyzeRequestRateBucketForRaw(value: Record<string, unknown>): Record<string, unknown> {
|
||||
return {
|
||||
startAt: stringOrNullValue(value.startAt),
|
||||
endAt: stringOrNullValue(value.endAt),
|
||||
count: numberOrNullValue(value.count),
|
||||
requestPerMinute: numberOrNullValue(value.requestPerMinute),
|
||||
valuesRedacted: true,
|
||||
};
|
||||
}
|
||||
|
||||
function compactWebObserveAnalyzeRequestRateSeriesForRaw(value: Record<string, unknown>): Record<string, unknown> {
|
||||
const out: Record<string, unknown> = {
|
||||
count: numberOrNullValue(value.count),
|
||||
bucketCount: numberOrNullValue(value.bucketCount),
|
||||
peakRequestPerMinute: numberOrNullValue(value.peakRequestPerMinute),
|
||||
peakBucket: compactWebObserveAnalyzeRequestRateBucketForRaw(recordValue(value.peakBucket)),
|
||||
valuesRedacted: true,
|
||||
};
|
||||
const pageKey = stringOrNullValue(value.pageKey);
|
||||
const apiKey = stringOrNullValue(value.apiKey);
|
||||
const path = stringOrNullValue(value.path);
|
||||
const method = stringOrNullValue(value.method);
|
||||
const pageRole = stringOrNullValue(value.pageRole);
|
||||
const pageId = stringOrNullValue(value.pageId);
|
||||
const pageEpoch = numberOrNullValue(value.pageEpoch);
|
||||
if (pageKey) out.pageKey = pageKey;
|
||||
if (pageRole) out.pageRole = pageRole;
|
||||
if (pageId) out.pageId = pageId;
|
||||
if (pageEpoch !== null) out.pageEpoch = pageEpoch;
|
||||
if (apiKey) out.apiKey = apiKey;
|
||||
if (method) out.method = method;
|
||||
if (path) out.path = path;
|
||||
return out;
|
||||
}
|
||||
|
||||
function compactWebObserveAnalyzeFailureForRaw(value: unknown): Record<string, unknown> | null {
|
||||
const failure = recordValue(value);
|
||||
if (Object.keys(failure).length === 0) return null;
|
||||
@@ -2818,6 +2894,10 @@ export function recoverWebObserveAnalyzeFromArtifacts(options: NodeWebProbeObser
|
||||
"const slimGroup = (item) => { const v = objectOrNull(item) || {}; return { count: v.count ?? null, method: clip(v.method, 12), status: v.status ?? null, path: clip(v.path ?? v.urlPath ?? v.route, 96), firstAt: v.firstAt ?? null, lastAt: v.lastAt ?? null, text: clip(v.text ?? v.preview, 180), failureKinds: arr(v.failureKinds).slice(0, 4).map((x) => clip(x, 48)), traceIds: arr(v.traceIds).slice(0, 3).map((x) => clip(x, 64)) }; };",
|
||||
"const slimSlowSample = (item) => { const v = objectOrNull(item) || {}; return { ts: v.ts ?? null, seq: v.seq ?? null, path: clip(v.path ?? v.rawPath, 96), initiatorType: clip(v.initiatorType, 24), durationMs: v.durationMs ?? null, requestToResponseStartMs: v.requestToResponseStartMs ?? v.streamOpenMs ?? null, responseTransferMs: v.responseTransferMs ?? null, timingStatus: clip(v.timingStatus, 16), nextHopProtocol: clip(v.nextHopProtocol, 24), serverTimingNames: arr(v.serverTimingNames).slice(0, 4).map((x) => clip(x, 32)), otelTraceId: clip(v.otelTraceId, 32) }; };",
|
||||
"const slimSlowApi = (item) => { const v = objectOrNull(item) || {}; return { path: clip(v.path ?? v.route, 96), route: clip(v.route ?? v.path, 96), sampleCount: v.sampleCount ?? null, p95Ms: v.p95Ms ?? v.p95 ?? null, maxMs: v.maxMs ?? v.max ?? null, budgetMs: v.budgetMs ?? null, overBudgetCount: v.overBudgetCount ?? null, overFiveSecondCount: v.overFiveSecondCount ?? null, slowSamples: arr(v.slowSamples).slice(0, 3).map(slimSlowSample) }; };",
|
||||
"const slimRequestBucket = (item) => { const v = objectOrNull(item) || {}; return { startAt: v.startAt ?? null, endAt: v.endAt ?? null, count: v.count ?? null, requestPerMinute: v.requestPerMinute ?? null, pageKey: clip(v.pageKey, 96), path: clip(v.path, 96), apiKey: clip(v.apiKey, 120), valuesRedacted: true }; };",
|
||||
"const slimRequestCurve = (item, isApi = false) => { const v = objectOrNull(item) || {}; return { pageKey: clip(v.pageKey, 96), pageRole: clip(v.pageRole, 24), pageId: clip(v.pageId, 32), pageEpoch: v.pageEpoch ?? null, apiKey: clip(v.apiKey, 120), method: clip(v.method, 12), path: clip(v.path, isApi ? 120 : 96), count: v.count ?? null, bucketCount: v.bucketCount ?? null, peakRequestPerMinute: v.peakRequestPerMinute ?? null, peakBucket: slimRequestBucket(v.peakBucket), buckets: arr(v.buckets).slice(-12).map(slimRequestBucket), valuesRedacted: true }; };",
|
||||
"const slimRequestPeak = (item) => { const v = objectOrNull(item) || {}; return { scope: clip(v.scope, 16), thresholdPerMinute: v.thresholdPerMinute ?? null, bucketMs: v.bucketMs ?? null, startAt: v.startAt ?? null, endAt: v.endAt ?? null, count: v.count ?? null, requestPerMinute: v.requestPerMinute ?? null, pageKey: clip(v.pageKey, 96), path: clip(v.path, 96), apiKey: clip(v.apiKey, 120), valuesRedacted: true }; };",
|
||||
"const slimRequestRateCurve = (value) => { const v = objectOrNull(value); if (!v) return null; return { summary: objectOrNull(v.summary) || v, buckets: arr(v.buckets).slice(-12).map(slimRequestBucket), pageCurves: arr(v.pageCurves).slice(0, 8).map((item) => slimRequestCurve(item, false)), apiPathCurves: arr(v.apiPathCurves).slice(0, 12).map((item) => slimRequestCurve(item, true)), peaks: arr(v.peaks).slice(0, 12).map(slimRequestPeak), valuesRedacted: true }; };",
|
||||
"const compactLoading = (value) => { const v = objectOrNull(value); if (!v) return null; const s = objectOrNull(v.summary) || {}; return { summary: s, longestSegments: arr(v.longestSegments ?? v.segments).slice(0, 8), owners: arr(v.owners).slice(0, 8), timeline: arr(v.timeline).slice(-12) }; };",
|
||||
"const compactSessionRailTitles = (value) => { const v = objectOrNull(value); if (!v) return null; return { summary: objectOrNull(v.summary) || {}, samples: arr(v.samples).slice(0, 8), examples: arr(v.examples).slice(0, 8) }; };",
|
||||
"const compactTraceOrder = (value) => { const v = objectOrNull(value); if (!v) return null; const s = objectOrNull(v.summary) || {}; return { summary: { sampleCount: v.sampleCount ?? s.sampleCount ?? null, traceRowCount: v.traceRowCount ?? s.traceRowCount ?? null, orderAnomalyCount: v.orderAnomalyCount ?? s.orderAnomalyCount ?? arr(v.orderAnomalies).length, completionNotLastCount: v.completionNotLastCount ?? s.completionNotLastCount ?? arr(v.completionNotLast).length }, orderAnomalies: arr(v.orderAnomalies).slice(0, 8) }; };",
|
||||
@@ -2832,10 +2912,13 @@ export function recoverWebObserveAnalyzeFromArtifacts(options: NodeWebProbeObser
|
||||
"const runtimeAlerts = objectOrNull(source.runtimeAlerts) || objectOrNull(recent.runtimeAlerts) || {};",
|
||||
"const browserProcess = objectOrNull(source.browserProcess) || objectOrNull(recent.browserProcess) || {};",
|
||||
"const promptNetwork = objectOrNull(source.promptNetwork) || objectOrNull(recent.promptNetwork) || {};",
|
||||
"const requestRateCurve = slimRequestRateCurve(source.requestRateCurve) || slimRequestRateCurve(source.requestRate) || slimRequestRateCurve(recent.requestRate);",
|
||||
"const requestRateSummary = objectOrNull(requestRateCurve?.summary) || objectOrNull(source.requestRate?.summary) || objectOrNull(source.requestRate) || null;",
|
||||
"const requestRatePeaks = arr(source.requestRatePeaks ?? requestRateCurve?.peaks ?? source.requestRate?.peaks ?? recent.requestRate?.peaks).slice(0, 12).map(slimRequestPeak);",
|
||||
"const archiveSummary = objectOrNull(source.archiveSummary) || {};",
|
||||
"const archiveSampleMetrics = objectOrNull(archiveSummary.sampleMetrics) || objectOrNull(source.sampleMetrics?.summary) || objectOrNull(srcMetrics.summary) || {};",
|
||||
"const slowApis = arr(source.pagePerformanceSlowApi).length > 0 ? arr(source.pagePerformanceSlowApi) : arr(pagePerformance.sameOriginApiByPath).filter((item) => Number(item?.overBudgetCount ?? item?.overFiveSecondCount ?? 0) > 0);",
|
||||
"const compact = { ok: source.ok === true, command: source.command ?? 'web-probe-observe analyze', stateDir: source.stateDir ?? stateDir, jsonlScope: source.jsonlScope ?? null, alertThresholds: source.alertThresholds ?? null, counts: source.counts ?? null, archiveSummary: { ...archiveSummary, sampleMetrics: archiveSampleMetrics, pagePerformance: objectOrNull(archiveSummary.pagePerformance) || objectOrNull(pagePerformance.summary) || {}, runtimeAlerts: objectOrNull(archiveSummary.runtimeAlerts) || objectOrNull(runtimeAlerts.summary) || {}, browserProcess: objectOrNull(archiveSummary.browserProcess) || objectOrNull(browserProcess.summary) || {}, redFindings: arr(archiveSummary.redFindings).slice(0, 12).map(slimFinding) }, analysisWindow: source.analysisWindow ?? objectOrNull(recent.summary), sampleMetrics: compactMetrics(srcMetrics), pageProvenance: objectOrNull(source.pageProvenance?.summary) || source.pageProvenance ?? null, pagePerformance: objectOrNull(pagePerformance.summary) || pagePerformance, projectManagement: objectOrNull(source.projectManagement) || null, promptNetwork: objectOrNull(promptNetwork.summary) || promptNetwork, runtimeAlerts: objectOrNull(runtimeAlerts.summary) || runtimeAlerts, browserProcess: objectOrNull(browserProcess.summary) || browserProcess, runnerErrors: arr(source.runnerErrors).slice(-8), commandFailures: arr(source.commandFailures).slice(-8), commandState: objectOrNull(source.commandState) || null, toolFindings: arr(source.toolFindings).slice(0, 8).map(slimFinding), httpErrorGroups: arr(source.httpErrorGroups ?? runtimeAlerts.networkHttpErrorsByPath).slice(0, 8).map(slimGroup), requestFailedGroups: arr(source.requestFailedGroups ?? runtimeAlerts.networkRequestFailedByPath).slice(0, 8).map(slimGroup), domDiagnosticGroups: arr(source.domDiagnosticGroups ?? runtimeAlerts.domDiagnosticsByText).slice(0, 5).map(slimGroup), domDiagnosticSamples: arr(source.domDiagnosticSamples ?? runtimeAlerts.domDiagnostics).slice(0, 8).map(slimGroup), consoleAlertGroups: arr(source.consoleAlertGroups ?? runtimeAlerts.consoleAlertsByPath).slice(0, 8).map(slimGroup), consoleAlertSamples: arr(source.consoleAlertSamples ?? runtimeAlerts.consoleAlerts).slice(0, 8).map(slimGroup), turnTimingRecentUpdateJumps: arr(source.turnTimingRecentUpdateJumps ?? srcMetrics.turnTimingRecentUpdateSawtoothJumps).slice(0, 8), turnTimingElapsedZeroResets: arr(source.turnTimingElapsedZeroResets ?? srcMetrics.turnTimingElapsedZeroResets).slice(0, 8), turnTimingTotalElapsedForwardJumps: arr(source.turnTimingTotalElapsedForwardJumps ?? srcMetrics.turnTimingTotalElapsedForwardJumps).slice(0, 8), pagePerformanceSlowApi: slowApis.slice(0, 8).map(slimSlowApi), archivePagePerformanceSlowApi: arr(source.archivePagePerformanceSlowApi).slice(0, 8).map(slimSlowApi), pagePerformancePartialApi: arr(source.pagePerformancePartialApi).slice(0, 8), pagePerformanceSseStreams: arr(source.pagePerformanceSseStreams).slice(0, 8), findings: arr(source.findings).slice(0, 12).map(slimFinding), archiveRedFindings: arr(source.archiveRedFindings ?? archiveSummary.redFindings).slice(0, 12).map(slimFinding), reportJsonPath: source.reportJsonPath ?? reportJsonPath, reportJsonSha256: source.reportJsonSha256 ?? sha256(reportJsonPath), reportMdPath: source.reportMdPath ?? reportMdPath, reportMdSha256: source.reportMdSha256 ?? sha256(reportMdPath), analyzer: { ...(objectOrNull(source.analyzer) || {}), recoveredFrom: 'analysis-artifact-after-transport-timeout', stdoutPath, stderrPath, stdoutBytes: statSize(stdoutPath), stderrBytes: statSize(stderrPath), reportJsonBytes: statSize(reportJsonPath), reportMdBytes: statSize(reportMdPath), transportExitCode: Number(transportExitRaw), transportTimedOut: transportTimedOutRaw === 'true', valuesRedacted: true }, valuesRedacted: true };",
|
||||
"const compact = { ok: source.ok === true, command: source.command ?? 'web-probe-observe analyze', stateDir: source.stateDir ?? stateDir, jsonlScope: source.jsonlScope ?? null, alertThresholds: source.alertThresholds ?? null, counts: source.counts ?? null, archiveSummary: { ...archiveSummary, sampleMetrics: archiveSampleMetrics, pagePerformance: objectOrNull(archiveSummary.pagePerformance) || objectOrNull(pagePerformance.summary) || {}, runtimeAlerts: objectOrNull(archiveSummary.runtimeAlerts) || objectOrNull(runtimeAlerts.summary) || {}, browserProcess: objectOrNull(archiveSummary.browserProcess) || objectOrNull(browserProcess.summary) || {}, requestRate: objectOrNull(archiveSummary.requestRate) || requestRateSummary || {}, redFindings: arr(archiveSummary.redFindings).slice(0, 12).map(slimFinding) }, analysisWindow: source.analysisWindow ?? objectOrNull(recent.summary), sampleMetrics: compactMetrics(srcMetrics), pageProvenance: objectOrNull(source.pageProvenance?.summary) || source.pageProvenance ?? null, pagePerformance: objectOrNull(pagePerformance.summary) || pagePerformance, projectManagement: objectOrNull(source.projectManagement) || null, promptNetwork: objectOrNull(promptNetwork.summary) || promptNetwork, requestRate: requestRateSummary, requestRateCurve, requestRatePeaks, runtimeAlerts: objectOrNull(runtimeAlerts.summary) || runtimeAlerts, browserProcess: objectOrNull(browserProcess.summary) || browserProcess, runnerErrors: arr(source.runnerErrors).slice(-8), commandFailures: arr(source.commandFailures).slice(-8), commandState: objectOrNull(source.commandState) || null, toolFindings: arr(source.toolFindings).slice(0, 8).map(slimFinding), httpErrorGroups: arr(source.httpErrorGroups ?? runtimeAlerts.networkHttpErrorsByPath).slice(0, 8).map(slimGroup), requestFailedGroups: arr(source.requestFailedGroups ?? runtimeAlerts.networkRequestFailedByPath).slice(0, 8).map(slimGroup), domDiagnosticGroups: arr(source.domDiagnosticGroups ?? runtimeAlerts.domDiagnosticsByText).slice(0, 5).map(slimGroup), domDiagnosticSamples: arr(source.domDiagnosticSamples ?? runtimeAlerts.domDiagnostics).slice(0, 8).map(slimGroup), consoleAlertGroups: arr(source.consoleAlertGroups ?? runtimeAlerts.consoleAlertsByPath).slice(0, 8).map(slimGroup), consoleAlertSamples: arr(source.consoleAlertSamples ?? runtimeAlerts.consoleAlerts).slice(0, 8).map(slimGroup), turnTimingRecentUpdateJumps: arr(source.turnTimingRecentUpdateJumps ?? srcMetrics.turnTimingRecentUpdateSawtoothJumps).slice(0, 8), turnTimingElapsedZeroResets: arr(source.turnTimingElapsedZeroResets ?? srcMetrics.turnTimingElapsedZeroResets).slice(0, 8), turnTimingTotalElapsedForwardJumps: arr(source.turnTimingTotalElapsedForwardJumps ?? srcMetrics.turnTimingTotalElapsedForwardJumps).slice(0, 8), pagePerformanceSlowApi: slowApis.slice(0, 8).map(slimSlowApi), archivePagePerformanceSlowApi: arr(source.archivePagePerformanceSlowApi).slice(0, 8).map(slimSlowApi), pagePerformancePartialApi: arr(source.pagePerformancePartialApi).slice(0, 8), pagePerformanceSseStreams: arr(source.pagePerformanceSseStreams).slice(0, 8), findings: arr(source.findings).slice(0, 12).map(slimFinding), archiveRedFindings: arr(source.archiveRedFindings ?? archiveSummary.redFindings).slice(0, 12).map(slimFinding), reportJsonPath: source.reportJsonPath ?? reportJsonPath, reportJsonSha256: source.reportJsonSha256 ?? sha256(reportJsonPath), reportMdPath: source.reportMdPath ?? reportMdPath, reportMdSha256: source.reportMdSha256 ?? sha256(reportMdPath), analyzer: { ...(objectOrNull(source.analyzer) || {}), recoveredFrom: 'analysis-artifact-after-transport-timeout', stdoutPath, stderrPath, stdoutBytes: statSize(stdoutPath), stderrBytes: statSize(stderrPath), reportJsonBytes: statSize(reportJsonPath), reportMdBytes: statSize(reportMdPath), transportExitCode: Number(transportExitRaw), transportTimedOut: transportTimedOutRaw === 'true', valuesRedacted: true }, valuesRedacted: true };",
|
||||
"console.log(JSON.stringify(compact));",
|
||||
"UNIDESK_WEB_OBSERVE_RECOVER_ANALYZE_ARTIFACT",
|
||||
].join("\n");
|
||||
|
||||
Reference in New Issue
Block a user