Skip to content

Commit 8a7d2a6

Browse files
author
Michael Lin
committed
feat(playground): add in-page debug stats panel
1 parent 71ec8aa commit 8a7d2a6

File tree

4 files changed

+204
-2
lines changed

4 files changed

+204
-2
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -225,7 +225,7 @@ RENDERIFY_RUNTIME_BROWSER_SANDBOX_FAIL_CLOSED=true pnpm playground
225225
RENDERIFY_LLM_USE_STRUCTURED_OUTPUT=false pnpm playground
226226
```
227227

228-
When debug mode is enabled, playground logs key inbound/outbound request summaries and exposes `GET /api/debug/stats` for request distribution diagnostics.
228+
When debug mode is enabled, playground logs key inbound/outbound request summaries, exposes `GET /api/debug/stats`, and shows an in-page **Debug Stats** panel with auto-refresh.
229229

230230
### Playground Hash Deep-Link
231231

packages/cli/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ renderify render-plan ./examples/runtime/recharts-dashboard-plan.json
5555
- `RENDERIFY_SECURITY_PROFILE` (`strict`, `balanced`, `relaxed`)
5656
- `RENDERIFY_PLAYGROUND_DEBUG` (`1`, `true`, `yes`, `on`)
5757

58-
When debug mode is enabled, playground prints key inbound/outbound request logs and exposes `/api/debug/stats` for request distribution snapshots.
58+
When debug mode is enabled, playground prints key inbound/outbound request logs, exposes `/api/debug/stats`, and renders an in-page **Debug Stats** panel with manual/auto refresh.
5959

6060
See `../../docs/getting-started.md` and `../../docs/security.md` for runtime and policy options.
6161

packages/cli/src/playground-html.ts

Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,15 @@ export const PLAYGROUND_HTML = `<!doctype html>
125125
font-size: 13px;
126126
}
127127
128+
.toggle {
129+
display: inline-flex;
130+
align-items: center;
131+
gap: 6px;
132+
font-size: 13px;
133+
color: var(--subtle);
134+
user-select: none;
135+
}
136+
128137
.render-output {
129138
min-height: 130px;
130139
border: 1px dashed rgba(15, 118, 110, 0.35);
@@ -193,6 +202,19 @@ export const PLAYGROUND_HTML = `<!doctype html>
193202
<h2>Streaming Feed</h2>
194203
<pre id="stream-output">[]</pre>
195204
</section>
205+
206+
<section class="card span-12">
207+
<h2>Debug Stats</h2>
208+
<div class="actions">
209+
<button id="refresh-debug" class="secondary">Refresh Debug Stats</button>
210+
<label class="toggle">
211+
<input id="auto-refresh-debug" type="checkbox" checked />
212+
Auto refresh (2s)
213+
</label>
214+
</div>
215+
<div class="status" id="debug-status">Waiting for debug stats...</div>
216+
<pre id="debug-output">{}</pre>
217+
</section>
196218
</div>
197219
</div>
198220
@@ -205,6 +227,10 @@ export const PLAYGROUND_HTML = `<!doctype html>
205227
const diagnosticsEl = byId("diagnostics");
206228
const streamOutputEl = byId("stream-output");
207229
const copyPlanLinkEl = byId("copy-plan-link");
230+
const refreshDebugEl = byId("refresh-debug");
231+
const autoRefreshDebugEl = byId("auto-refresh-debug");
232+
const debugStatusEl = byId("debug-status");
233+
const debugOutputEl = byId("debug-output");
208234
209235
const controls = [
210236
byId("run-prompt"),
@@ -225,6 +251,10 @@ export const PLAYGROUND_HTML = `<!doctype html>
225251
statusEl.textContent = text;
226252
};
227253
254+
const setDebugStatus = (text) => {
255+
debugStatusEl.textContent = text;
256+
};
257+
228258
const safeJson = (value) => {
229259
try {
230260
return JSON.stringify(value ?? {}, null, 2);
@@ -244,6 +274,8 @@ export const PLAYGROUND_HTML = `<!doctype html>
244274
let interactiveBlobModuleUrl = null;
245275
let babelStandalonePromise;
246276
let diagnosticsSnapshot = {};
277+
let debugRefreshTimer = null;
278+
let debugRefreshInFlight = false;
247279
248280
const resetInteractiveMount = () => {
249281
interactiveMountVersion += 1;
@@ -274,6 +306,146 @@ export const PLAYGROUND_HTML = `<!doctype html>
274306
});
275307
};
276308
309+
const toDebugCount = (value) =>
310+
typeof value === "number" && Number.isFinite(value) && value >= 0
311+
? value
312+
: 0;
313+
314+
const compactDebugSnapshot = (payload) => {
315+
const inbound = isRecord(payload && payload.inbound) ? payload.inbound : {};
316+
const outbound = isRecord(payload && payload.outbound) ? payload.outbound : {};
317+
const inboundRoutes = Array.isArray(inbound.routes) ? inbound.routes : [];
318+
const outboundTargets = Array.isArray(outbound.targets) ? outbound.targets : [];
319+
const recent = Array.isArray(payload && payload.recent) ? payload.recent : [];
320+
321+
return {
322+
enabled: payload && payload.enabled === true,
323+
startedAt:
324+
payload && typeof payload.startedAt === "string"
325+
? payload.startedAt
326+
: undefined,
327+
uptimeMs: toDebugCount(payload && payload.uptimeMs),
328+
inbound: {
329+
totalRequests: toDebugCount(inbound.totalRequests),
330+
routes: inboundRoutes.slice(0, 10),
331+
},
332+
outbound: {
333+
totalRequests: toDebugCount(outbound.totalRequests),
334+
targets: outboundTargets.slice(0, 10),
335+
},
336+
recent: recent.slice(-25),
337+
...(payload && payload.error ? { error: String(payload.error) } : {}),
338+
};
339+
};
340+
341+
const setDebugOutput = (payload) => {
342+
debugOutputEl.textContent = safeJson(compactDebugSnapshot(payload));
343+
};
344+
345+
const formatDebugSummary = (snapshot) => {
346+
if (!snapshot.enabled) {
347+
return snapshot.error
348+
? "Debug stats unavailable: " + snapshot.error
349+
: "Debug stats unavailable.";
350+
}
351+
352+
const inboundTotal = toDebugCount(
353+
snapshot.inbound && snapshot.inbound.totalRequests,
354+
);
355+
const outboundTotal = toDebugCount(
356+
snapshot.outbound && snapshot.outbound.totalRequests,
357+
);
358+
return (
359+
"Debug mode enabled. inbound=" +
360+
inboundTotal +
361+
", outbound=" +
362+
outboundTotal +
363+
". Updated " +
364+
new Date().toLocaleTimeString() +
365+
"."
366+
);
367+
};
368+
369+
async function requestDebugStats() {
370+
const response = await fetch("/api/debug/stats", {
371+
method: "GET",
372+
cache: "no-store",
373+
});
374+
375+
let payload = {};
376+
try {
377+
payload = await response.json();
378+
} catch {
379+
payload = {};
380+
}
381+
382+
if (!response.ok) {
383+
const message =
384+
isRecord(payload) && typeof payload.error === "string"
385+
? payload.error
386+
: "request failed with status " + response.status;
387+
return {
388+
enabled: false,
389+
error: message,
390+
statusCode: response.status,
391+
};
392+
}
393+
394+
return isRecord(payload) ? payload : {};
395+
}
396+
397+
async function refreshDebugStats(options = {}) {
398+
const silent = options && options.silent === true;
399+
if (debugRefreshInFlight) {
400+
return;
401+
}
402+
403+
debugRefreshInFlight = true;
404+
if (!silent) {
405+
setDebugStatus("Refreshing debug stats...");
406+
}
407+
408+
try {
409+
const payload = await requestDebugStats();
410+
setDebugOutput(payload);
411+
setDebugStatus(formatDebugSummary(payload));
412+
if (
413+
payload &&
414+
payload.enabled !== true &&
415+
typeof payload.error === "string" &&
416+
payload.error.toLowerCase().includes("debug mode is disabled")
417+
) {
418+
autoRefreshDebugEl.checked = false;
419+
restartDebugAutoRefresh();
420+
}
421+
} catch (error) {
422+
const message = error instanceof Error ? error.message : String(error);
423+
const snapshot = {
424+
enabled: false,
425+
error: message,
426+
};
427+
setDebugOutput(snapshot);
428+
setDebugStatus("Debug stats unavailable: " + message);
429+
} finally {
430+
debugRefreshInFlight = false;
431+
}
432+
}
433+
434+
const restartDebugAutoRefresh = () => {
435+
if (debugRefreshTimer) {
436+
clearInterval(debugRefreshTimer);
437+
debugRefreshTimer = null;
438+
}
439+
440+
if (!(autoRefreshDebugEl && autoRefreshDebugEl.checked)) {
441+
return;
442+
}
443+
444+
debugRefreshTimer = setInterval(() => {
445+
void refreshDebugStats({ silent: true });
446+
}, 2000);
447+
};
448+
277449
const ensureBabelStandalone = async () => {
278450
if (window.Babel && typeof window.Babel.transform === "function") {
279451
return;
@@ -732,6 +904,10 @@ export const PLAYGROUND_HTML = `<!doctype html>
732904
if (!response.ok) {
733905
throw new Error(payload && payload.error ? String(payload.error) : "request failed");
734906
}
907+
908+
if (path !== "/api/debug/stats") {
909+
void refreshDebugStats({ silent: true });
910+
}
735911
return payload;
736912
}
737913
@@ -787,6 +963,7 @@ export const PLAYGROUND_HTML = `<!doctype html>
787963
throw error;
788964
} finally {
789965
setBusy(false);
966+
void refreshDebugStats({ silent: true });
790967
}
791968
}
792969
@@ -810,6 +987,7 @@ export const PLAYGROUND_HTML = `<!doctype html>
810987
diagnosticsEl.textContent = String(error);
811988
} finally {
812989
setBusy(false);
990+
void refreshDebugStats({ silent: true });
813991
}
814992
}
815993
@@ -895,6 +1073,7 @@ export const PLAYGROUND_HTML = `<!doctype html>
8951073
diagnosticsEl.textContent = String(error);
8961074
} finally {
8971075
setBusy(false);
1076+
void refreshDebugStats({ silent: true });
8981077
}
8991078
}
9001079
@@ -933,6 +1112,7 @@ export const PLAYGROUND_HTML = `<!doctype html>
9331112
diagnosticsEl.textContent = String(error);
9341113
} finally {
9351114
setBusy(false);
1115+
void refreshDebugStats({ silent: true });
9361116
}
9371117
}
9381118
@@ -941,6 +1121,7 @@ export const PLAYGROUND_HTML = `<!doctype html>
9411121
htmlOutputEl.innerHTML = "";
9421122
writeDiagnostics({});
9431123
streamOutputEl.textContent = "[]";
1124+
void refreshDebugStats({ silent: true });
9441125
setStatus("Cleared.");
9451126
}
9461127
@@ -1006,15 +1187,29 @@ export const PLAYGROUND_HTML = `<!doctype html>
10061187
byId("stream-prompt").addEventListener("click", streamPrompt);
10071188
byId("run-plan").addEventListener("click", runPlan);
10081189
byId("probe-plan").addEventListener("click", probePlan);
1190+
refreshDebugEl.addEventListener("click", () => {
1191+
void refreshDebugStats();
1192+
});
1193+
autoRefreshDebugEl.addEventListener("change", () => {
1194+
restartDebugAutoRefresh();
1195+
});
10091196
copyPlanLinkEl.addEventListener("click", () => {
10101197
void copyPlanLink();
10111198
});
10121199
byId("clear").addEventListener("click", clearAll);
10131200
1201+
restartDebugAutoRefresh();
1202+
void refreshDebugStats({ silent: true });
10141203
void renderFromHashPayload();
10151204
window.addEventListener("hashchange", () => {
10161205
void renderFromHashPayload();
10171206
});
1207+
window.addEventListener("beforeunload", () => {
1208+
if (debugRefreshTimer) {
1209+
clearInterval(debugRefreshTimer);
1210+
debugRefreshTimer = null;
1211+
}
1212+
});
10181213
</script>
10191214
</body>
10201215
</html>`;

tests/e2e/e2e.test.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -459,6 +459,13 @@ test("e2e: playground debug mode exposes inbound/outbound request distribution",
459459
try {
460460
await waitForHealth(`${baseUrl}/api/health`, 10000);
461461

462+
const pageResponse = await fetch(`${baseUrl}/`);
463+
assert.equal(pageResponse.status, 200);
464+
const pageHtml = await pageResponse.text();
465+
assert.match(pageHtml, /id="refresh-debug"/);
466+
assert.match(pageHtml, /id="debug-status"/);
467+
assert.match(pageHtml, /id="debug-output"/);
468+
462469
const promptResponse = await fetchJson(`${baseUrl}/api/prompt`, {
463470
method: "POST",
464471
body: {

0 commit comments

Comments
 (0)