feat(diagnostics): the debug recorder — start, the pill, steps, screenshots, stop (E16 S2)
- the start dialog is the consent: level (Részletes/Teljes), what is recorded, retention and the cap (now served as diagnostics.session_minutes) - the pill docks under the toasts: elapsed time, Step (an optional label, its own Escape layer), Screenshot (tab capture where the browser has it), Stop; compact on a phone - while recording, lib/log tags every event and every request path (req, beacon, uploadReq via one identityHeaders) names the session; applyDiagnostics raises the level for the session's run and never lowers a raised one, and lapses at whichever ends first - Stop sends what is waiting before the server closes the session; the cap says so and opens the composer - Settings → Diagnosztika and the command palette start/step/stop it - the evidence leaves the log upload's own request lines out - the step shortcut is deliberately palette-only: the keymap's D1 rule bars a new modifier chord, and a bare letter collides with the players'
This commit is contained in:
@@ -1,7 +1,8 @@
|
||||
// The user's diagnostics level (E22 S5) and the browser log it lets out. Only touches core's req and
|
||||
// beacon. The streaming itself — what is sent, when, and when it stops — is `lib/diagnostics.ts`.
|
||||
import type { LogEvent } from "../log";
|
||||
import { beacon, req } from "./core";
|
||||
import type { ServerLogRow } from "./admin";
|
||||
import { beacon, getActiveAccount, identityHeaders, req } from "./core";
|
||||
|
||||
/** Alap / Részletes / Teljes. The server decides how long a raised level lasts. */
|
||||
export type DiagnosticsLevel = "INFO" | "DEBUG" | "TRACE";
|
||||
@@ -12,6 +13,8 @@ export interface DiagnosticsState {
|
||||
until: string | null;
|
||||
/** How many days this instance keeps stored log lines — the consent text states it. */
|
||||
retention_days: number;
|
||||
/** How long a debug session records at most — the start dialog's consent text states it. */
|
||||
session_minutes: number;
|
||||
}
|
||||
|
||||
/** Where batches go. The shipper leaves the log lines ABOUT this request out of what it sends, or
|
||||
@@ -33,3 +36,65 @@ export const diagnosticsApi = {
|
||||
/** The last batch of a closing tab (keepalive, fire-and-forget). */
|
||||
sendLogEventsOnUnload: (events: LogEvent[]): void => beacon(DIAGNOSTICS_EVENTS_PATH, { events }),
|
||||
};
|
||||
|
||||
// --- debug sessions (E16) ----------------------------------------------------------------------
|
||||
|
||||
/** The two levels a recording can run at — it always records more than the base. */
|
||||
export type DebugSessionLevel = "DEBUG" | "TRACE";
|
||||
|
||||
/** One recording: Start → reproduce → Stop. `until` is the server's cap; past it the session is
|
||||
* over whether or not it was stopped. */
|
||||
export interface DebugSession {
|
||||
id: string;
|
||||
level: DebugSessionLevel;
|
||||
started_at: string;
|
||||
until: string;
|
||||
ended_at: string | null;
|
||||
ended_reason: "stopped" | "expired" | null;
|
||||
/** The issue it was sent as, once sent. */
|
||||
issue_url: string | null;
|
||||
screenshots: number;
|
||||
}
|
||||
|
||||
export interface DebugScreenshot {
|
||||
id: number;
|
||||
mime: string;
|
||||
size: number;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
const SESSIONS = "/api/debug-sessions";
|
||||
|
||||
export const debugSessionsApi = {
|
||||
startDebugSession: (level: DebugSessionLevel): Promise<{ ok: true; session: DebugSession }> =>
|
||||
req(SESSIONS, { method: "POST", body: JSON.stringify({ level }) }),
|
||||
stopDebugSession: (id: string): Promise<{ ok: true; session: DebugSession }> =>
|
||||
req(`${SESSIONS}/${id}/stop`, { method: "POST" }),
|
||||
debugSessions: (): Promise<{ items: DebugSession[] }> => req(SESSIONS),
|
||||
debugSession: (id: string): Promise<DebugSession> => req(`${SESSIONS}/${id}`),
|
||||
/** The rows an issue from this session would carry, browser and server, in time order. */
|
||||
debugSessionEvents: (id: string): Promise<{ items: ServerLogRow[]; total: number }> =>
|
||||
req(`${SESSIONS}/${id}/events`),
|
||||
debugScreenshots: (id: string): Promise<{ items: DebugScreenshot[] }> =>
|
||||
req(`${SESSIONS}/${id}/screenshots`),
|
||||
/** One image as the raw body — the server checks what it really is, whatever `type` says. */
|
||||
addDebugScreenshot: (
|
||||
id: string,
|
||||
image: Blob,
|
||||
): Promise<{ ok: true; screenshot: DebugScreenshot }> =>
|
||||
req(`${SESSIONS}/${id}/screenshots`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": image.type || "application/octet-stream", ...identityHeaders() },
|
||||
body: image,
|
||||
}),
|
||||
removeDebugScreenshot: (id: string, shotId: number): Promise<{ ok: true; deleted: number }> =>
|
||||
req(`${SESSIONS}/${id}/screenshots/${shotId}`, { method: "DELETE" }),
|
||||
/** Where an `<img>` loads a screenshot from. An `<img>` cannot send the account header, so the
|
||||
* tab's account rides as `?account=` (the wallet-gated selection downloads use); without it a
|
||||
* per-tab account's screenshot resolves to the session default and 404s. */
|
||||
debugScreenshotUrl: (id: string, shotId: number): string => {
|
||||
const a = getActiveAccount();
|
||||
const url = `${SESSIONS}/${id}/screenshots/${shotId}`;
|
||||
return a != null ? `${url}?account=${a}` : url;
|
||||
},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user