diff --git a/scripts/src/hwlab-node-web-sentinel-p5-observe.ts b/scripts/src/hwlab-node-web-sentinel-p5-observe.ts index e5331173..1f525d44 100644 --- a/scripts/src/hwlab-node-web-sentinel-p5-observe.ts +++ b/scripts/src/hwlab-node-web-sentinel-p5-observe.ts @@ -800,6 +800,7 @@ function compactQuickVerifyRecordAnalysis(value: unknown): Record): }; } +function compactQuickVerifyRecordRequestRate(summaryValue: Record, curveValue: Record): Record | null { + const source = Object.keys(curveValue).length > 0 ? curveValue : summaryValue; + const summary = record(curveValue.summary); + const effectiveSummary = Object.keys(summary).length > 0 ? summary : summaryValue; + const pageCurves = Array.isArray(curveValue.pageCurves) ? curveValue.pageCurves.map((item) => compactQuickVerifyRecordRequestRateCurve(item, "page")).filter((item) => item.buckets.length > 0).slice(0, 12) : []; + const apiPathCurves = Array.isArray(curveValue.apiPathCurves) ? curveValue.apiPathCurves.map((item) => compactQuickVerifyRecordRequestRateCurve(item, "apiPath")).filter((item) => item.buckets.length > 0).slice(0, 16) : []; + const buckets = Array.isArray(curveValue.buckets) ? curveValue.buckets.map(compactQuickVerifyRecordRequestRateBucket).filter((item) => item.startAt !== null && item.requestPerMinute !== null).slice(-120) : []; + if (Object.keys(source).length === 0 && pageCurves.length === 0 && apiPathCurves.length === 0 && buckets.length === 0) return null; + return { + summary: compactQuickVerifyRecordRequestRateSummary(effectiveSummary), + buckets, + pageCurves, + apiPathCurves, + peaks: Array.isArray(source.peaks) ? source.peaks.slice(0, 12).map(compactQuickVerifyRecordRequestRatePeak) : [], + valuesRedacted: true, + }; +} + +function compactQuickVerifyRecordRequestRateSummary(value: Record): Record { + return { + bucketMs: numberAtNullable(value, "bucketMs"), + bucketSeconds: numberAtNullable(value, "bucketSeconds"), + requestCount: numberAtNullable(value, "requestCount"), + bucketCount: numberAtNullable(value, "bucketCount"), + pageCount: numberAtNullable(value, "pageCount"), + apiPathCount: numberAtNullable(value, "apiPathCount"), + firstAt: stringAtNullable(value, "firstAt"), + lastAt: stringAtNullable(value, "lastAt"), + totalRedPerMinute: numberAtNullable(value, "totalRedPerMinute"), + pageRedPerMinute: numberAtNullable(value, "pageRedPerMinute"), + apiPathRedPerMinute: numberAtNullable(value, "apiPathRedPerMinute"), + totalPeakPerMinute: numberAtNullable(value, "totalPeakPerMinute"), + totalPeakCount: numberAtNullable(value, "totalPeakCount"), + totalPeakAt: stringAtNullable(value, "totalPeakAt"), + pagePeakPerMinute: numberAtNullable(value, "pagePeakPerMinute"), + pagePeakKey: stringAtNullable(value, "pagePeakKey"), + pagePeakPath: stringAtNullable(value, "pagePeakPath"), + apiPathPeakPerMinute: numberAtNullable(value, "apiPathPeakPerMinute"), + apiPathPeakKey: stringAtNullable(value, "apiPathPeakKey"), + overThresholdPeakCount: numberAtNullable(value, "overThresholdPeakCount"), + valuesRedacted: true, + }; +} + +function compactQuickVerifyRecordRequestRateCurve(value: unknown, scope: string): Record & { buckets: Record[] } { + const item = record(value); + const buckets = Array.isArray(item.buckets) ? item.buckets.map(compactQuickVerifyRecordRequestRateBucket).filter((bucket) => bucket.startAt !== null && bucket.requestPerMinute !== null).slice(-120) : []; + return { + pageKey: stringAtNullable(item, "pageKey"), + pageRole: stringAtNullable(item, "pageRole"), + pageId: stringAtNullable(item, "pageId"), + pageEpoch: numberAtNullable(item, "pageEpoch"), + apiKey: stringAtNullable(item, "apiKey"), + method: stringAtNullable(item, "method"), + path: stringAtNullable(item, "path"), + count: numberAtNullable(item, "count"), + bucketCount: numberAtNullable(item, "bucketCount") ?? buckets.length, + peakRequestPerMinute: numberAtNullable(item, "peakRequestPerMinute"), + peakBucket: compactQuickVerifyRecordRequestRateBucket(record(item.peakBucket)), + buckets, + scope, + valuesRedacted: true, + }; +} + +function compactQuickVerifyRecordRequestRateBucket(value: unknown): Record { + const item = record(value); + return { + bucketStartMs: numberAtNullable(item, "bucketStartMs"), + bucketEndMs: numberAtNullable(item, "bucketEndMs"), + startAt: stringAtNullable(item, "startAt"), + endAt: stringAtNullable(item, "endAt"), + count: numberAtNullable(item, "count"), + requestPerMinute: numberAtNullable(item, "requestPerMinute"), + valuesRedacted: true, + }; +} + +function compactQuickVerifyRecordRequestRatePeak(value: unknown): Record { + const item = record(value); + return { + scope: stringAtNullable(item, "scope"), + thresholdPerMinute: numberAtNullable(item, "thresholdPerMinute"), + overThreshold: item.overThreshold === true, + bucketMs: numberAtNullable(item, "bucketMs"), + startAt: stringAtNullable(item, "startAt"), + endAt: stringAtNullable(item, "endAt"), + count: numberAtNullable(item, "count"), + requestPerMinute: numberAtNullable(item, "requestPerMinute"), + method: stringAtNullable(item, "method"), + path: stringAtNullable(item, "path"), + apiKey: stringAtNullable(item, "apiKey"), + pageKey: stringAtNullable(item, "pageKey"), + pageRole: stringAtNullable(item, "pageRole"), + pageId: stringAtNullable(item, "pageId"), + pageEpoch: numberAtNullable(item, "pageEpoch"), + valuesRedacted: true, + }; +} + function compactQuickVerifyRecordMemorySeries(value: unknown): Record & { points: Record[] } { const item = record(value); const points = Array.isArray(item.points) @@ -1021,6 +1122,9 @@ function analysisSummaryFromAnalyzeResult(analysis: ChildCliResult, fallbackStat analysisWindow: record(source.analysisWindow), pagePerformanceSlowApi: Array.isArray(source.pagePerformanceSlowApi) ? source.pagePerformanceSlowApi.slice(0, 8).map(record) : [], browserProcess: compactAnalyzeBrowserProcess(source.browserProcess), + requestRate: record(source.requestRate), + requestRateCurve: record(source.requestRateCurve), + requestRatePeaks: Array.isArray(source.requestRatePeaks) ? source.requestRatePeaks.slice(0, 12).map(record) : [], result: record(payload.result), valuesRedacted: true, }; diff --git a/scripts/src/hwlab-node-web-sentinel-service.ts b/scripts/src/hwlab-node-web-sentinel-service.ts index 8f3d4507..78a1cfd9 100644 --- a/scripts/src/hwlab-node-web-sentinel-service.ts +++ b/scripts/src/hwlab-node-web-sentinel-service.ts @@ -1102,11 +1102,15 @@ function dashboardRunMemoryDetail(config: WebProbeSentinelServiceConfig, row: Re function dashboardRunRequestRateDetail(config: WebProbeSentinelServiceConfig, row: Record, stored: Record): Record { const options = dashboardDetailRequestRateOptions(config); - const storedRequestRate = compactDashboardRequestRate(record(record(record(stored.summary).analysis).requestRate), options, "recorded-analysis-summary"); + const storedAnalysis = record(record(stored.summary).analysis); + const storedRequestRate = compactDashboardRequestRate(record(storedAnalysis.requestRate), options, "recorded-analysis-summary"); if (storedRequestRate.ok === true && hasDashboardRequestRateSeries(storedRequestRate)) return storedRequestRate; + const storedRequestRateCurve = compactDashboardRequestRate(record(storedAnalysis.requestRateCurve), options, "recorded-analysis-request-rate-curve"); + if (storedRequestRateCurve.ok === true && hasDashboardRequestRateSeries(storedRequestRateCurve)) return storedRequestRateCurve; const fromArtifacts = readDashboardRequestRateFromAnalysisReport(config, row, options); if (fromArtifacts.ok === true) return fromArtifacts; if (storedRequestRate.ok === true) return storedRequestRate; + if (storedRequestRateCurve.ok === true) return storedRequestRateCurve; return { ok: false, source: "unavailable", @@ -1120,7 +1124,7 @@ function dashboardRunRequestRateDetail(config: WebProbeSentinelServiceConfig, ro pageSeries: [], apiPathSeries: [], peaks: [], - reason: fromArtifacts.reason ?? storedRequestRate.reason ?? "request-rate-samples-missing", + reason: fromArtifacts.reason ?? storedRequestRate.reason ?? storedRequestRateCurve.reason ?? "request-rate-samples-missing", valuesRedacted: true, }; }