fix(web-probe): capture web performance diagnostics payloads
This commit is contained in:
@@ -0,0 +1,214 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { mkdir, mkdtemp, readFile, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { test } from "bun:test";
|
||||
|
||||
import { nodeWebObserveAnalyzerSource } from "../hwlab-node-web-observe-analyzer-source";
|
||||
import { nodeWebObserveCollectViewNodeScript } from "../hwlab-node-web-observe-collect";
|
||||
|
||||
const alertThresholds = {
|
||||
sameOriginApiSlowMs: 60000,
|
||||
partialApiSlowMs: 60000,
|
||||
longLivedStreamOpenSlowMs: 60000,
|
||||
visibleLoadingSlowMs: 60000,
|
||||
turnTimingSampleSlackSeconds: 60,
|
||||
turnElapsedSevereTimeoutSeconds: 3600,
|
||||
domEvaluateTimeoutRedCount: 99,
|
||||
domEvaluateTimeoutRedWindowMs: 60000,
|
||||
screenshotTimeoutRedCount: 99,
|
||||
pageErrorRedCount: 99,
|
||||
longTaskRedMs: 1000,
|
||||
longAnimationFrameRedMs: 1000,
|
||||
eventLoopGapRedMs: 1000,
|
||||
browserProcessSampleIntervalMs: 1000,
|
||||
requestRateBucketMs: 10000,
|
||||
requestRateTotalRedPerMinute: 999999,
|
||||
requestRatePageRedPerMinute: 999999,
|
||||
requestRateApiPathRedPerMinute: 999999,
|
||||
browserTotalRssRedMb: 999999,
|
||||
browserProcessRssRedMb: 999999,
|
||||
browserRssGrowthRedMb: 999999,
|
||||
browserRssGrowthWindowMs: 60000,
|
||||
playwrightResponsivenessRedMs: 60000,
|
||||
playwrightResponsivenessTimeoutRedCount: 99,
|
||||
cdpMetricsTimeoutRedCount: 99,
|
||||
uncommandedStateChangeCommandWindowMs: 1000,
|
||||
scrollJumpCommandWindowMs: 1000,
|
||||
scrollJumpFromY: 999999,
|
||||
scrollJumpToY: 999999,
|
||||
sessionRailFallbackRatio: 0.5,
|
||||
};
|
||||
|
||||
const browserFreezePolicy = {
|
||||
enabled: true,
|
||||
blockerWindowMs: 60000,
|
||||
memory: { totalRssBlockerMb: 999999, processRssBlockerMb: 999999, growthBlockerMb: 999999 },
|
||||
responsiveness: { latencyBlockerMs: 60000, eventBlockerCount: 99 },
|
||||
cdp: { metricsTimeoutBlockerCount: 99 },
|
||||
kill: { enabled: false, gracefulSignal: "SIGTERM", forceSignal: "SIGKILL", graceMs: 1000, pollIntervalMs: 100, exitCode: 124 },
|
||||
};
|
||||
|
||||
function shellQuote(value: string): string {
|
||||
return `'${value.replace(/'/g, `'\\''`)}'`;
|
||||
}
|
||||
|
||||
async function writeJsonl(path: string, rows: Array<Record<string, unknown>>): Promise<void> {
|
||||
await writeFile(path, rows.map((row) => JSON.stringify(row)).join("\n") + (rows.length > 0 ? "\n" : ""));
|
||||
}
|
||||
|
||||
async function runAnalyzer(stateDir: string): Promise<Record<string, unknown>> {
|
||||
const analyzerPath = join(stateDir, "analyze.mjs");
|
||||
await writeFile(analyzerPath, nodeWebObserveAnalyzerSource(), { mode: 0o700 });
|
||||
const result = spawnSync("bun", [analyzerPath, stateDir], {
|
||||
cwd: join(import.meta.dir, "../../.."),
|
||||
env: {
|
||||
...process.env,
|
||||
UNIDESK_WEB_OBSERVE_ANALYZE_TAIL_SAMPLES: "0",
|
||||
UNIDESK_WEB_OBSERVE_ALERT_THRESHOLDS_JSON: JSON.stringify(alertThresholds),
|
||||
UNIDESK_WEB_OBSERVE_BROWSER_FREEZE_POLICY_JSON: JSON.stringify(browserFreezePolicy),
|
||||
},
|
||||
encoding: "utf8",
|
||||
});
|
||||
assert.equal(result.status, 0, result.stderr || result.stdout);
|
||||
return JSON.parse(await readFile(join(stateDir, "analysis", "report.json"), "utf8")) as Record<string, unknown>;
|
||||
}
|
||||
|
||||
async function writeState(): Promise<string> {
|
||||
const stateDir = await mkdtemp(join(tmpdir(), "unidesk-web-observe-web-performance-"));
|
||||
await mkdir(join(stateDir, "analysis"), { recursive: true });
|
||||
await writeFile(join(stateDir, "manifest.json"), JSON.stringify({ jobId: "webobs-web-performance-test", status: "completed" }) + "\n");
|
||||
await writeFile(join(stateDir, "heartbeat.json"), JSON.stringify({ status: "completed", updatedAt: "2026-07-02T17:00:05.000Z" }) + "\n");
|
||||
await writeJsonl(join(stateDir, "samples.jsonl"), []);
|
||||
await writeJsonl(join(stateDir, "console.jsonl"), []);
|
||||
await writeJsonl(join(stateDir, "errors.jsonl"), []);
|
||||
await writeJsonl(join(stateDir, "browser-process.jsonl"), []);
|
||||
await writeJsonl(join(stateDir, "performance-events.jsonl"), []);
|
||||
await writeJsonl(join(stateDir, "artifacts.jsonl"), []);
|
||||
await writeJsonl(join(stateDir, "control.jsonl"), [{
|
||||
type: "sendPrompt",
|
||||
phase: "completed",
|
||||
commandId: "cmd-prompt",
|
||||
ts: "2026-07-02T17:00:00.000Z",
|
||||
}]);
|
||||
await writeJsonl(join(stateDir, "network.jsonl"), [
|
||||
{
|
||||
type: "request",
|
||||
ts: "2026-07-02T17:00:01.000Z",
|
||||
pageRole: "control",
|
||||
pageId: "page-1",
|
||||
method: "POST",
|
||||
url: "https://hwlab.example.test/v1/web-performance",
|
||||
resourceType: "fetch",
|
||||
webPerformancePayload: {
|
||||
captureStatus: "captured",
|
||||
parseStatus: "parsed",
|
||||
schemaVersion: "hwlab-web-performance-v2",
|
||||
byteCount: 768,
|
||||
byteLimit: 65536,
|
||||
bodyHash: "sha256:payload",
|
||||
eventCount: 1,
|
||||
storedEventCount: 1,
|
||||
events: [{
|
||||
ts: "2026-07-02T17:00:01.000Z",
|
||||
eventType: "runtime_diagnostic",
|
||||
diagnosticCode: "workbench_sse_flush",
|
||||
reason: "sse_flush",
|
||||
module: "workbench-runtime",
|
||||
traceId: "trc_webperf_fixture",
|
||||
sessionIdHash: "sha256:session",
|
||||
eventCount: 12,
|
||||
chunkCount: 3,
|
||||
flushDurationMs: 18.75,
|
||||
droppedCount: 2,
|
||||
maxItemsPerChunk: 5,
|
||||
maxChunkMs: 7.5,
|
||||
replacedByKey: true,
|
||||
valuesRedacted: true,
|
||||
}],
|
||||
valuesRedacted: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "request",
|
||||
ts: "2026-07-02T17:00:02.000Z",
|
||||
method: "POST",
|
||||
url: "https://hwlab.example.test/v1/web-performance",
|
||||
webPerformancePayload: {
|
||||
captureStatus: "captured",
|
||||
parseStatus: "invalid-json",
|
||||
byteCount: 12,
|
||||
byteLimit: 65536,
|
||||
bodyHash: "sha256:invalid",
|
||||
eventCount: null,
|
||||
events: [],
|
||||
valuesRedacted: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "request",
|
||||
ts: "2026-07-02T17:00:03.000Z",
|
||||
method: "POST",
|
||||
url: "https://hwlab.example.test/v1/web-performance",
|
||||
webPerformancePayload: {
|
||||
captureStatus: "skipped-over-limit",
|
||||
parseStatus: "not-parsed-over-limit",
|
||||
byteCount: 70000,
|
||||
byteLimit: 65536,
|
||||
bodyHash: "sha256:large",
|
||||
eventCount: null,
|
||||
events: [],
|
||||
valuesRedacted: true,
|
||||
},
|
||||
},
|
||||
]);
|
||||
return stateDir;
|
||||
}
|
||||
|
||||
test("analyzer and performance-summary include bounded web-performance runtime diagnostics", async () => {
|
||||
const stateDir = await writeState();
|
||||
const report = await runAnalyzer(stateDir);
|
||||
const runtimeAlerts = report.runtimeAlerts as Record<string, unknown>;
|
||||
const summary = runtimeAlerts.summary as Record<string, unknown>;
|
||||
assert.equal(summary.webPerformancePayloadRequestCount, 3);
|
||||
assert.equal(summary.webPerformancePayloadParsedCount, 1);
|
||||
assert.equal(summary.webPerformancePayloadParseIssueCount, 2);
|
||||
assert.equal(summary.webPerformanceRuntimeDiagnosticCount, 1);
|
||||
|
||||
const groups = runtimeAlerts.webPerformanceRuntimeDiagnosticsByCode as Array<Record<string, unknown>>;
|
||||
assert.equal(groups[0].diagnosticCode, "workbench_sse_flush");
|
||||
assert.equal(groups[0].reason, "sse_flush");
|
||||
assert.equal(groups[0].module, "workbench-runtime");
|
||||
assert.equal(groups[0].eventCount, 12);
|
||||
assert.equal(groups[0].chunkCount, 3);
|
||||
assert.equal(groups[0].maxFlushDurationMs, 18.75);
|
||||
assert.equal(groups[0].replacedByKeyCount, 1);
|
||||
|
||||
const reportText = JSON.stringify(report);
|
||||
assert.match(reportText, /workbench_sse_flush/u);
|
||||
assert.doesNotMatch(reportText, /rawBody|super-secret|ses_unredacted/u);
|
||||
|
||||
const script = nodeWebObserveCollectViewNodeScript({
|
||||
maxFiles: 100,
|
||||
view: "performance-summary",
|
||||
traceId: null,
|
||||
sampleSeq: null,
|
||||
timestamp: null,
|
||||
turn: null,
|
||||
commandId: null,
|
||||
windowMs: null,
|
||||
});
|
||||
const result = spawnSync("bash", ["-lc", `state_dir=${shellQuote(stateDir)}\n${script}`], {
|
||||
cwd: join(import.meta.dir, "../../.."),
|
||||
encoding: "utf8",
|
||||
});
|
||||
assert.equal(result.status, 0, result.stderr || result.stdout);
|
||||
const output = JSON.parse(result.stdout);
|
||||
const renderedText = String(output.renderedText ?? "");
|
||||
assert.match(renderedText, /Web performance runtime diagnostics/u);
|
||||
assert.match(renderedText, /payloads=3 parsed=1 parseIssues=2 events=1 groups=1/u);
|
||||
assert.match(renderedText, /workbench_sse_flush reason=sse_flush module=workbench-runtime/u);
|
||||
assert.match(renderedText, /eventCount=12 chunks=3 flushMax=18.75ms dropped=2/u);
|
||||
assert.match(renderedText, /payload states=invalid-json:1, not-parsed-over-limit:1, parsed:1/u);
|
||||
}, 20_000);
|
||||
Reference in New Issue
Block a user