feat: add web probe performance hotspot analysis
This commit is contained in:
@@ -0,0 +1,458 @@
|
||||
// SPEC: PJ2026-01040111 long-running Workbench observation.
|
||||
// Responsibility: Analyzer recent-window, page provenance, and Navigation Timing API performance source fragment.
|
||||
|
||||
export function nodeWebObserveAnalyzerWindowPageSource(): string {
|
||||
return String.raw`function buildRecentAnalysisWindow({ samples, control, network, consoleEvents, errors, artifacts, browserProcessRows, performanceRows, manifest }) {
|
||||
const latestSampleMs = latestTimestampMs(samples);
|
||||
const windowMs = 5 * 60 * 1000;
|
||||
const fromMs = Number.isFinite(latestSampleMs) ? latestSampleMs - windowMs : Number.NEGATIVE_INFINITY;
|
||||
const toMs = Number.isFinite(latestSampleMs) ? latestSampleMs : Number.POSITIVE_INFINITY;
|
||||
const inWindow = (item) => {
|
||||
const tsMs = Date.parse(item?.ts);
|
||||
return Number.isFinite(tsMs) && tsMs >= fromMs && tsMs <= toMs;
|
||||
};
|
||||
const windowSamples = samples.filter(inWindow);
|
||||
const windowControl = control.filter(inWindow);
|
||||
const windowNetwork = network.filter(inWindow);
|
||||
const windowConsole = consoleEvents.filter(inWindow);
|
||||
const windowErrors = errors.filter(inWindow);
|
||||
const windowArtifacts = (artifacts || []).filter(inWindow);
|
||||
const windowBrowserProcessRows = (browserProcessRows || []).filter(inWindow);
|
||||
const windowPerformanceRows = (performanceRows || []).filter(inWindow);
|
||||
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 frontendPerformance = buildFrontendPerformanceReport(windowPerformanceRows, windowArtifacts);
|
||||
const findings = buildFindings(windowSamples, control, windowNetwork, windowErrors, sampleMetrics, promptNetwork, runtimeAlerts, pagePerformance, requestRate, pageProvenance, [], {}, apiDomLag, browserProcess, frontendPerformance);
|
||||
return {
|
||||
summary: {
|
||||
name: "recent-5m",
|
||||
windowMs,
|
||||
fromAt: Number.isFinite(fromMs) ? new Date(fromMs).toISOString() : null,
|
||||
toAt: Number.isFinite(toMs) ? new Date(toMs).toISOString() : null,
|
||||
samples: windowSamples.length,
|
||||
control: windowControl.length,
|
||||
network: windowNetwork.length,
|
||||
console: windowConsole.length,
|
||||
errors: windowErrors.length,
|
||||
artifacts: windowArtifacts.length,
|
||||
browserProcess: windowBrowserProcessRows.length,
|
||||
performance: windowPerformanceRows.length,
|
||||
valuesRedacted: true
|
||||
},
|
||||
sampleMetrics,
|
||||
pageProvenance,
|
||||
pagePerformance,
|
||||
requestRate,
|
||||
promptNetwork,
|
||||
runtimeAlerts,
|
||||
apiDomLag,
|
||||
browserProcess,
|
||||
frontendPerformance,
|
||||
findings,
|
||||
valuesRedacted: true
|
||||
};
|
||||
}
|
||||
|
||||
function latestTimestampMs(items) {
|
||||
let latest = Number.NEGATIVE_INFINITY;
|
||||
for (const item of items || []) {
|
||||
const tsMs = Date.parse(item?.ts);
|
||||
if (Number.isFinite(tsMs) && tsMs > latest) latest = tsMs;
|
||||
}
|
||||
return latest;
|
||||
}
|
||||
|
||||
function buildPageProvenanceReport(samples, control, manifest) {
|
||||
const groups = new Map();
|
||||
for (const sample of samples) {
|
||||
const provenance = sample?.pageProvenance;
|
||||
if (!provenance) continue;
|
||||
const key = provenance.assetFingerprint || "unknown";
|
||||
const group = groups.get(key) || {
|
||||
assetFingerprint: provenance.assetFingerprint || null,
|
||||
pageLoadSeqs: [],
|
||||
sampleCount: 0,
|
||||
firstSeq: sample.seq ?? null,
|
||||
lastSeq: sample.seq ?? null,
|
||||
firstAt: sample.ts ?? null,
|
||||
lastAt: sample.ts ?? null,
|
||||
urlPaths: [],
|
||||
scriptCount: provenance.scriptCount ?? null,
|
||||
stylesheetCount: provenance.stylesheetCount ?? null,
|
||||
metaCount: provenance.metaCount ?? null,
|
||||
scripts: Array.isArray(provenance.scripts) ? provenance.scripts.slice(0, 12) : [],
|
||||
stylesheets: Array.isArray(provenance.stylesheets) ? provenance.stylesheets.slice(0, 12) : [],
|
||||
valuesRedacted: true
|
||||
};
|
||||
group.sampleCount += 1;
|
||||
group.lastSeq = sample.seq ?? null;
|
||||
group.lastAt = sample.ts ?? null;
|
||||
if (provenance.pageLoadSeq !== null && provenance.pageLoadSeq !== undefined && !group.pageLoadSeqs.includes(provenance.pageLoadSeq)) group.pageLoadSeqs.push(provenance.pageLoadSeq);
|
||||
if (provenance.urlPath && !group.urlPaths.includes(provenance.urlPath)) group.urlPaths.push(provenance.urlPath);
|
||||
groups.set(key, group);
|
||||
}
|
||||
const segments = Array.from(groups.values()).sort((a, b) => Number(a.firstSeq ?? 0) - Number(b.firstSeq ?? 0));
|
||||
const controlSegments = control
|
||||
.filter((item) => item.type === "page-provenance" || item?.pageProvenance)
|
||||
.map((item) => ({
|
||||
ts: item.ts ?? null,
|
||||
reason: item.reason ?? item.detail?.reason ?? null,
|
||||
httpStatus: item.httpStatus ?? item.detail?.httpStatus ?? null,
|
||||
pageProvenance: item.pageProvenance ?? item.detail?.pageProvenance ?? null,
|
||||
}))
|
||||
.slice(0, 80);
|
||||
return {
|
||||
summary: {
|
||||
segmentCount: segments.length,
|
||||
sampleCount: segments.reduce((sum, item) => sum + item.sampleCount, 0),
|
||||
manifestFingerprint: manifest?.pageProvenance?.assetFingerprint ?? null,
|
||||
controlSegmentCount: controlSegments.length
|
||||
},
|
||||
segments,
|
||||
controlSegments,
|
||||
valuesRedacted: true
|
||||
};
|
||||
}
|
||||
|
||||
function buildPagePerformanceReport(samples, manifest) {
|
||||
const base = manifest?.baseUrl || "http://invalid.local";
|
||||
const seen = new Set();
|
||||
const groups = new Map();
|
||||
const sampleTimes = samples.map((sample) => Date.parse(sample?.ts || "")).filter(Number.isFinite);
|
||||
const windowStartMs = sampleTimes.length > 0 ? Math.min(...sampleTimes) : null;
|
||||
const windowEndMs = sampleTimes.length > 0 ? Math.max(...sampleTimes) : null;
|
||||
for (const sample of samples) {
|
||||
const entries = Array.isArray(sample?.performance) ? sample.performance : [];
|
||||
for (const entry of entries) {
|
||||
const durationMs = Number(entry?.duration);
|
||||
if (!Number.isFinite(durationMs) || durationMs < 0) continue;
|
||||
const entryCompletedMs = performanceEntryCompletedEpochMs(sample, entry);
|
||||
if (windowStartMs !== null && entryCompletedMs !== null && entryCompletedMs < windowStartMs) continue;
|
||||
if (windowEndMs !== null && entryCompletedMs !== null && entryCompletedMs > windowEndMs + 1000) continue;
|
||||
const entryTs = entryCompletedMs === null ? (sample.ts ?? null) : new Date(entryCompletedMs).toISOString();
|
||||
const parsed = parsePerformanceUrl(entry?.name, base);
|
||||
if (!parsed.sameOrigin || !isApiLikePath(parsed.path)) continue;
|
||||
const normalizedPath = normalizeApiPath(parsed.path);
|
||||
const routeKind = classifyApiPerformanceRoute(normalizedPath, entry);
|
||||
const isLongLivedStream = routeKind === "same-origin-api-stream";
|
||||
const streamOpenMs = streamOpenLatencyMs(entry);
|
||||
const timingStatus = resourceTimingPhaseStatus(entry);
|
||||
const dedupeKey = [parsed.path, entry.initiatorType || "", sample?.pageProvenance?.pageLoadSeq ?? "", sample?.pageProvenance?.timeOrigin ?? "", entry.startTime ?? "", Math.round(durationMs)].join("|");
|
||||
if (seen.has(dedupeKey)) continue;
|
||||
seen.add(dedupeKey);
|
||||
const group = groups.get(normalizedPath) || {
|
||||
routeKind,
|
||||
path: normalizedPath,
|
||||
isLongLivedStream,
|
||||
budgetMetric: isLongLivedStream ? "streamOpenMs" : "durationMs",
|
||||
rawPathSamples: [],
|
||||
sampleCount: 0,
|
||||
completeTimingSampleCount: 0,
|
||||
partialTimingSampleCount: 0,
|
||||
durationsMs: [],
|
||||
streamOpenDurationsMs: [],
|
||||
overFiveSecondCount: 0,
|
||||
overBudgetCount: 0,
|
||||
partialOverFiveSecondCount: 0,
|
||||
partialOverBudgetCount: 0,
|
||||
streamLifetimeOverFiveSecondCount: 0,
|
||||
streamOpenOverFiveSecondCount: 0,
|
||||
streamOpenOverBudgetCount: 0,
|
||||
firstAt: entryTs,
|
||||
lastAt: entryTs,
|
||||
firstSeq: sample.seq ?? null,
|
||||
lastSeq: sample.seq ?? null,
|
||||
initiatorTypes: [],
|
||||
pageAssetFingerprints: [],
|
||||
slowSamples: [],
|
||||
partialSamples: [],
|
||||
valuesRedacted: true
|
||||
};
|
||||
group.sampleCount += 1;
|
||||
const partialOrdinaryTiming = !isLongLivedStream && timingStatus.status !== "complete";
|
||||
let overBudget = false;
|
||||
if (partialOrdinaryTiming) {
|
||||
group.partialTimingSampleCount += 1;
|
||||
if (durationMs > 5000) group.partialOverFiveSecondCount += 1;
|
||||
if (durationMs > alertThresholds.partialApiSlowMs) {
|
||||
group.partialOverBudgetCount += 1;
|
||||
if (group.partialSamples.length < 80) group.partialSamples.push(compactPagePerformanceSlowSample({ sample, entry, entryTs, normalizedPath, rawPath: parsed.path, durationMs, streamOpenMs }));
|
||||
}
|
||||
} else {
|
||||
group.completeTimingSampleCount += 1;
|
||||
group.durationsMs.push(durationMs);
|
||||
if (isLongLivedStream) {
|
||||
if (durationMs > 5000) group.streamLifetimeOverFiveSecondCount += 1;
|
||||
if (streamOpenMs !== null) {
|
||||
group.streamOpenDurationsMs.push(streamOpenMs);
|
||||
if (streamOpenMs > 5000) {
|
||||
group.streamOpenOverFiveSecondCount += 1;
|
||||
group.overFiveSecondCount += 1;
|
||||
}
|
||||
if (streamOpenMs > alertThresholds.longLivedStreamOpenSlowMs) {
|
||||
group.streamOpenOverBudgetCount += 1;
|
||||
group.overBudgetCount += 1;
|
||||
overBudget = true;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (durationMs > 5000) group.overFiveSecondCount += 1;
|
||||
if (durationMs > alertThresholds.sameOriginApiSlowMs) {
|
||||
group.overBudgetCount += 1;
|
||||
overBudget = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (overBudget && group.slowSamples.length < 80) group.slowSamples.push(compactPagePerformanceSlowSample({ sample, entry, entryTs, normalizedPath, rawPath: parsed.path, durationMs, streamOpenMs }));
|
||||
group.lastAt = entryTs;
|
||||
group.lastSeq = sample.seq ?? null;
|
||||
if (parsed.path && !group.rawPathSamples.includes(parsed.path)) group.rawPathSamples.push(parsed.path);
|
||||
if (entry.initiatorType && !group.initiatorTypes.includes(entry.initiatorType)) group.initiatorTypes.push(entry.initiatorType);
|
||||
const assetFingerprint = sample?.pageProvenance?.assetFingerprint;
|
||||
if (assetFingerprint && !group.pageAssetFingerprints.includes(assetFingerprint)) group.pageAssetFingerprints.push(assetFingerprint);
|
||||
groups.set(normalizedPath, group);
|
||||
}
|
||||
}
|
||||
const sameOriginApiByPath = Array.from(groups.values()).map((group) => {
|
||||
const durations = group.durationsMs.slice().sort((a, b) => a - b);
|
||||
const streamOpenDurations = group.streamOpenDurationsMs.slice().sort((a, b) => a - b);
|
||||
return {
|
||||
routeKind: group.routeKind,
|
||||
path: group.path,
|
||||
isLongLivedStream: group.isLongLivedStream === true,
|
||||
budgetMetric: group.budgetMetric,
|
||||
sampleCount: group.sampleCount,
|
||||
budgetMs: group.isLongLivedStream === true ? alertThresholds.longLivedStreamOpenSlowMs : alertThresholds.sameOriginApiSlowMs,
|
||||
partialBudgetMs: alertThresholds.partialApiSlowMs,
|
||||
streamOpenBudgetMs: alertThresholds.longLivedStreamOpenSlowMs,
|
||||
completeTimingSampleCount: group.completeTimingSampleCount,
|
||||
partialTimingSampleCount: group.partialTimingSampleCount,
|
||||
p50Ms: percentile(durations, 50),
|
||||
p75Ms: percentile(durations, 75),
|
||||
p95Ms: percentile(durations, 95),
|
||||
maxMs: durations.length > 0 ? durations[durations.length - 1] : null,
|
||||
streamOpenSampleCount: streamOpenDurations.length,
|
||||
streamOpenP50Ms: percentile(streamOpenDurations, 50),
|
||||
streamOpenP75Ms: percentile(streamOpenDurations, 75),
|
||||
streamOpenP95Ms: percentile(streamOpenDurations, 95),
|
||||
streamOpenMaxMs: streamOpenDurations.length > 0 ? streamOpenDurations[streamOpenDurations.length - 1] : null,
|
||||
streamOpenOverFiveSecondCount: group.streamOpenOverFiveSecondCount,
|
||||
streamOpenOverBudgetCount: group.streamOpenOverBudgetCount,
|
||||
streamLifetimeOverFiveSecondCount: group.streamLifetimeOverFiveSecondCount,
|
||||
overFiveSecondCount: group.overFiveSecondCount,
|
||||
overBudgetCount: group.overBudgetCount,
|
||||
partialOverFiveSecondCount: group.partialOverFiveSecondCount,
|
||||
partialOverBudgetCount: group.partialOverBudgetCount,
|
||||
overFiveSecondRatio: group.sampleCount > 0 ? Number((group.overFiveSecondCount / group.sampleCount).toFixed(3)) : 0,
|
||||
overBudgetRatio: group.sampleCount > 0 ? Number((group.overBudgetCount / group.sampleCount).toFixed(3)) : 0,
|
||||
firstAt: group.firstAt,
|
||||
lastAt: group.lastAt,
|
||||
firstSeq: group.firstSeq,
|
||||
lastSeq: group.lastSeq,
|
||||
initiatorTypes: group.initiatorTypes,
|
||||
rawPathSamples: group.rawPathSamples.slice(0, 8),
|
||||
pageAssetFingerprints: group.pageAssetFingerprints.slice(0, 8),
|
||||
slowSamples: group.slowSamples
|
||||
.slice()
|
||||
.sort((a, b) => Number(b.durationMs ?? 0) - Number(a.durationMs ?? 0))
|
||||
.slice(0, 12),
|
||||
partialSamples: group.partialSamples
|
||||
.slice()
|
||||
.sort((a, b) => Number(b.durationMs ?? 0) - Number(a.durationMs ?? 0))
|
||||
.slice(0, 12),
|
||||
valuesRedacted: true
|
||||
};
|
||||
}).sort((a, b) => (Number(b.overBudgetCount ?? b.overFiveSecondCount ?? 0) - Number(a.overBudgetCount ?? a.overFiveSecondCount ?? 0)) || (Number(b.p95Ms ?? 0) - Number(a.p95Ms ?? 0)) || a.path.localeCompare(b.path));
|
||||
const longLivedStreams = sameOriginApiByPath.filter((item) => item.isLongLivedStream);
|
||||
const ordinaryApi = sameOriginApiByPath.filter((item) => item.isLongLivedStream !== true);
|
||||
const slow = ordinaryApi.filter((item) => Number(item.overBudgetCount ?? item.overFiveSecondCount ?? 0) > 0);
|
||||
const slowFiveSecond = ordinaryApi.filter((item) => Number(item.overFiveSecondCount ?? 0) > 0);
|
||||
const partialSlow = ordinaryApi.filter((item) => Number(item.partialOverBudgetCount ?? item.partialOverFiveSecondCount ?? 0) > 0);
|
||||
const partialFiveSecond = ordinaryApi.filter((item) => Number(item.partialOverFiveSecondCount ?? 0) > 0);
|
||||
const slowStreamOpen = longLivedStreams.filter((item) => Number(item.streamOpenOverBudgetCount ?? item.streamOpenOverFiveSecondCount ?? 0) > 0);
|
||||
const slowStreamOpenFiveSecond = longLivedStreams.filter((item) => Number(item.streamOpenOverFiveSecondCount ?? 0) > 0);
|
||||
const budgetP95Values = sameOriginApiByPath
|
||||
.map((item) => Number(item.isLongLivedStream ? (item.streamOpenP95Ms ?? 0) : (item.p95Ms ?? 0)))
|
||||
.filter((value) => Number.isFinite(value));
|
||||
return {
|
||||
summary: {
|
||||
budgetMs: alertThresholds.sameOriginApiSlowMs,
|
||||
alertThresholds,
|
||||
sameOriginApiPathCount: sameOriginApiByPath.length,
|
||||
sameOriginApiSampleCount: sameOriginApiByPath.reduce((sum, item) => sum + item.sampleCount, 0),
|
||||
longLivedStreamPathCount: longLivedStreams.length,
|
||||
longLivedStreamSampleCount: longLivedStreams.reduce((sum, item) => sum + item.sampleCount, 0),
|
||||
longLivedStreamOpenOverFiveSecondPathCount: slowStreamOpenFiveSecond.length,
|
||||
longLivedStreamOpenOverFiveSecondSampleCount: slowStreamOpenFiveSecond.reduce((sum, item) => sum + Number(item.streamOpenOverFiveSecondCount ?? 0), 0),
|
||||
longLivedStreamOpenOverBudgetPathCount: slowStreamOpen.length,
|
||||
longLivedStreamOpenOverBudgetSampleCount: slowStreamOpen.reduce((sum, item) => sum + Number(item.streamOpenOverBudgetCount ?? item.streamOpenOverFiveSecondCount ?? 0), 0),
|
||||
longLivedStreamLifetimeOverFiveSecondSampleCount: longLivedStreams.reduce((sum, item) => sum + Number(item.streamLifetimeOverFiveSecondCount ?? 0), 0),
|
||||
slowPathCount: slow.length,
|
||||
slowSampleCount: slow.reduce((sum, item) => sum + Number(item.overBudgetCount ?? item.overFiveSecondCount ?? 0), 0),
|
||||
overFiveSecondPathCount: slowFiveSecond.length,
|
||||
overFiveSecondSampleCount: slowFiveSecond.reduce((sum, item) => sum + Number(item.overFiveSecondCount ?? 0), 0),
|
||||
partialTimingSampleCount: ordinaryApi.reduce((sum, item) => sum + Number(item.partialTimingSampleCount ?? 0), 0),
|
||||
partialOverFiveSecondPathCount: partialFiveSecond.length,
|
||||
partialOverFiveSecondSampleCount: partialFiveSecond.reduce((sum, item) => sum + Number(item.partialOverFiveSecondCount ?? 0), 0),
|
||||
partialOverBudgetPathCount: partialSlow.length,
|
||||
partialOverBudgetSampleCount: partialSlow.reduce((sum, item) => sum + Number(item.partialOverBudgetCount ?? item.partialOverFiveSecondCount ?? 0), 0),
|
||||
worstP95Ms: budgetP95Values.length > 0 ? Math.max(...budgetP95Values) : null,
|
||||
valuesRedacted: true
|
||||
},
|
||||
sameOriginApiByPath,
|
||||
valuesRedacted: true
|
||||
};
|
||||
}
|
||||
|
||||
function performanceEntryCompletedEpochMs(sample, entry) {
|
||||
const origin = Number(sample?.pageProvenance?.timeOrigin);
|
||||
const responseEnd = Number(entry?.responseEnd);
|
||||
const startTime = Number(entry?.startTime);
|
||||
const offset = Number.isFinite(responseEnd) && responseEnd > 0 ? responseEnd : startTime;
|
||||
if (Number.isFinite(origin) && origin > 0 && Number.isFinite(offset) && offset >= 0) return Math.round(origin + offset);
|
||||
const sampleTs = Date.parse(sample?.ts || "");
|
||||
return Number.isFinite(sampleTs) ? sampleTs : null;
|
||||
}
|
||||
|
||||
function compactPagePerformanceSlowSample({ sample, entry, entryTs, normalizedPath, rawPath, durationMs, streamOpenMs }) {
|
||||
const timingStatus = resourceTimingPhaseStatus(entry);
|
||||
const serverTiming = compactServerTiming(entry?.serverTiming);
|
||||
return {
|
||||
ts: entryTs ?? sample?.ts ?? null,
|
||||
sampleTs: sample?.ts ?? null,
|
||||
seq: sample?.seq ?? null,
|
||||
path: normalizedPath ?? null,
|
||||
rawPath: rawPath ?? null,
|
||||
initiatorType: entry?.initiatorType ?? null,
|
||||
durationMs: roundFinite(durationMs),
|
||||
startTimeMs: roundFinite(entry?.startTime),
|
||||
fetchStartMs: roundFinite(entry?.fetchStart),
|
||||
requestStartMs: roundFinite(entry?.requestStart),
|
||||
responseStartMs: roundFinite(entry?.responseStart),
|
||||
responseEndMs: roundFinite(entry?.responseEnd),
|
||||
streamOpenMs: roundFinite(streamOpenMs),
|
||||
dnsMs: phaseDeltaMs(entry, "domainLookupEnd", "domainLookupStart"),
|
||||
tcpMs: phaseDeltaMs(entry, "connectEnd", "connectStart"),
|
||||
tlsStartMs: roundFinite(entry?.secureConnectionStart),
|
||||
requestToResponseStartMs: phaseDeltaMs(entry, "responseStart", "requestStart"),
|
||||
responseTransferMs: phaseDeltaMs(entry, "responseEnd", "responseStart"),
|
||||
timingStatus: timingStatus.status,
|
||||
invalidTimingPhases: timingStatus.invalidPhases,
|
||||
partialTimingPhases: timingStatus.partialPhases,
|
||||
transferSize: Number.isFinite(Number(entry?.transferSize)) ? Number(entry.transferSize) : null,
|
||||
encodedBodySize: Number.isFinite(Number(entry?.encodedBodySize)) ? Number(entry.encodedBodySize) : null,
|
||||
decodedBodySize: Number.isFinite(Number(entry?.decodedBodySize)) ? Number(entry.decodedBodySize) : null,
|
||||
nextHopProtocol: entry?.nextHopProtocol ?? null,
|
||||
serverTiming,
|
||||
serverTimingNames: serverTiming.map((item) => item.name).filter(Boolean).slice(0, 8),
|
||||
otelTraceId: extractOtelTraceIdFromServerTiming(serverTiming),
|
||||
valuesRedacted: true
|
||||
};
|
||||
}
|
||||
|
||||
function phaseDeltaMs(entry, endKey, startKey) {
|
||||
const end = Number(entry?.[endKey]);
|
||||
const start = Number(entry?.[startKey]);
|
||||
if (!Number.isFinite(end) || !Number.isFinite(start) || end <= 0 || start <= 0 || end < start) return null;
|
||||
return Math.round(end - start);
|
||||
}
|
||||
|
||||
function resourceTimingPhaseStatus(entry) {
|
||||
const pairs = [
|
||||
["requestToResponseStart", "requestStart", "responseStart"],
|
||||
["responseTransfer", "responseStart", "responseEnd"],
|
||||
];
|
||||
const invalidPhases = [];
|
||||
const partialPhases = [];
|
||||
for (const [label, startKey, endKey] of pairs) {
|
||||
const start = Number(entry?.[startKey]);
|
||||
const end = Number(entry?.[endKey]);
|
||||
if (!Number.isFinite(start) || !Number.isFinite(end) || start <= 0 || end <= 0) {
|
||||
partialPhases.push(label);
|
||||
} else if (end < start) {
|
||||
invalidPhases.push(label);
|
||||
}
|
||||
}
|
||||
return {
|
||||
status: invalidPhases.length > 0 ? "invalid" : (partialPhases.length > 0 ? "partial" : "complete"),
|
||||
invalidPhases,
|
||||
partialPhases,
|
||||
};
|
||||
}
|
||||
|
||||
function compactServerTiming(value) {
|
||||
const items = Array.isArray(value) ? value : [];
|
||||
return items.slice(0, 8).map((item) => ({
|
||||
name: truncate(String(item?.name || ""), 80),
|
||||
duration: Number.isFinite(Number(item?.duration)) ? Math.round(Number(item.duration)) : null,
|
||||
description: truncate(String(item?.description || ""), 120),
|
||||
})).filter((item) => item.name || item.description || item.duration !== null);
|
||||
}
|
||||
|
||||
function extractOtelTraceIdFromServerTiming(items) {
|
||||
const text = (Array.isArray(items) ? items : []).map((item) => [item.name, item.description].filter(Boolean).join(" ")).join(" ");
|
||||
const match = text.match(/\b[0-9a-f]{32}\b/iu);
|
||||
return match ? match[0].toLowerCase() : null;
|
||||
}
|
||||
|
||||
function roundFinite(value) {
|
||||
const numeric = Number(value);
|
||||
return Number.isFinite(numeric) ? Math.round(numeric) : null;
|
||||
}
|
||||
|
||||
function classifyApiPerformanceRoute(normalizedPath, entry = {}) {
|
||||
if (normalizedPath === "/v1/workbench/events") return "same-origin-api-stream";
|
||||
if (String(entry?.initiatorType ?? "").toLowerCase() === "eventsource") return "same-origin-api-stream";
|
||||
return "same-origin-api";
|
||||
}
|
||||
|
||||
function streamOpenLatencyMs(entry = {}) {
|
||||
const responseStart = Number(entry?.responseStart);
|
||||
const startTime = Number(entry?.startTime);
|
||||
if (!Number.isFinite(responseStart) || responseStart <= 0) return null;
|
||||
if (!Number.isFinite(startTime) || startTime < 0) return Math.max(0, responseStart);
|
||||
if (responseStart < startTime) return null;
|
||||
return Math.max(0, responseStart - startTime);
|
||||
}
|
||||
|
||||
function parsePerformanceUrl(value, base) {
|
||||
try {
|
||||
const url = new URL(String(value || ""), base);
|
||||
const origin = new URL(String(base || "http://invalid.local")).origin;
|
||||
return { sameOrigin: url.origin === origin, path: url.pathname };
|
||||
} catch {
|
||||
return { sameOrigin: false, path: "-" };
|
||||
}
|
||||
}
|
||||
|
||||
function isApiLikePath(path) {
|
||||
return /^\/(?:v1(?:\/|$)|auth(?:\/|$)|health(?:\/|$))/u.test(String(path || ""));
|
||||
}
|
||||
|
||||
function normalizeApiPath(path) {
|
||||
return String(path || "-")
|
||||
.replace(/\/v1\/workbench\/sessions\/ses_[^/]+/gu, "/v1/workbench/sessions/:id")
|
||||
.replace(/\/v1\/workbench\/turns\/trc_[^/]+/gu, "/v1/workbench/turns/:traceId")
|
||||
.replace(/\/v1\/workbench\/traces\/trc_[^/]+/gu, "/v1/workbench/traces/:traceId")
|
||||
.replace(/\/v1\/workbench\/sessions\/[0-9a-f-]{12,}/giu, "/v1/workbench/sessions/:id")
|
||||
.replace(/\/v1\/[^/]+\/[0-9a-f-]{16,}(?=\/|$)/giu, (match) => match.replace(/\/[0-9a-f-]{16,}$/iu, "/:id"));
|
||||
}
|
||||
|
||||
function percentile(sortedValues, percentileValue) {
|
||||
if (!Array.isArray(sortedValues) || sortedValues.length === 0) return null;
|
||||
if (sortedValues.length === 1) return Math.round(sortedValues[0]);
|
||||
const rank = (percentileValue / 100) * (sortedValues.length - 1);
|
||||
const lower = Math.floor(rank);
|
||||
const upper = Math.ceil(rank);
|
||||
if (lower === upper) return Math.round(sortedValues[lower]);
|
||||
const weight = rank - lower;
|
||||
return Math.round(sortedValues[lower] * (1 - weight) + sortedValues[upper] * weight);
|
||||
}
|
||||
`;
|
||||
}
|
||||
Reference in New Issue
Block a user