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:
@@ -8,7 +8,7 @@ import { formatBytes } from "../format";
|
||||
import { reportError } from "../errorDialog";
|
||||
import { activeAccountId, pinActiveAccount } from "../activeAccount";
|
||||
import { logRequest, QUIET_HEADER, REQUEST_ID_HEADER, ROUTE_HEADER, send } from "../http";
|
||||
import { newRequestId } from "../log";
|
||||
import { currentDebugSession, newRequestId } from "../log";
|
||||
|
||||
/** The active UI language, as a bare code the server understands ("en"/"hu" — never "en-GB").
|
||||
* Lives here so the request builders can stamp it themselves: a signal that has to be passed in by
|
||||
@@ -253,24 +253,35 @@ export const setupHeaders = (token: string) => ({
|
||||
// sessionStorage (per-tab, survives F5, not shared across tabs). null = use the session default.
|
||||
// The storage key + reader live in storage.ts (so its per-account helpers can use them too).
|
||||
const ACTIVE_ACCOUNT_HEADER = "X-Siftlode-Account";
|
||||
// The debug session this tab is recording (E16): the server tags its own lines of the request with
|
||||
// it, and logs them at the session's level. Ignored by the server unless it is the caller's own.
|
||||
const DEBUG_SESSION_HEADER = "X-Debug-Session";
|
||||
|
||||
export const getActiveAccount = activeAccountId;
|
||||
|
||||
/** Who this request is from — the tab's account and the debug session it records into — as the
|
||||
* headers every request path sends (`req`, `beacon`, `uploadReq`): one definition, so a path
|
||||
* cannot quietly go out without them. */
|
||||
export function identityHeaders(): Record<string, string> {
|
||||
const active = getActiveAccount();
|
||||
const session = currentDebugSession();
|
||||
return {
|
||||
...(active != null ? { [ACTIVE_ACCOUNT_HEADER]: String(active) } : {}),
|
||||
...(session ? { [DEBUG_SESSION_HEADER]: session } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
// Fire-and-forget POST that survives page unload (keepalive) — shared by the resume-position
|
||||
// beacons (YT + Plex players). A normal fetch is cancelled on unload and React effect cleanup
|
||||
// doesn't run on a full reload, so without keepalive the last position (esp. right after a seek)
|
||||
// would be lost.
|
||||
export function beacon(url: string, body: Record<string, unknown>): void {
|
||||
const active = getActiveAccount();
|
||||
try {
|
||||
void send(url, {
|
||||
method: "POST",
|
||||
keepalive: true,
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
...(active != null ? { [ACTIVE_ACCOUNT_HEADER]: String(active) } : {}),
|
||||
},
|
||||
headers: { "Content-Type": "application/json", ...identityHeaders() },
|
||||
body: JSON.stringify(body),
|
||||
}).catch(() => {});
|
||||
} catch {
|
||||
@@ -292,16 +303,15 @@ export async function req<T = unknown>(
|
||||
for (;;) {
|
||||
let r: Response;
|
||||
try {
|
||||
const active = getActiveAccount();
|
||||
r = await send(
|
||||
url,
|
||||
{
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
// Per-tab identity override (only setup calls pass their own headers, and those are
|
||||
// pre-auth, so this is safe to put in the defaults that `...opts` may replace).
|
||||
...(active != null ? { [ACTIVE_ACCOUNT_HEADER]: String(active) } : {}),
|
||||
// Per-tab identity override (only setup calls and raw uploads pass their own headers:
|
||||
// setup is pre-auth, and an upload spreads `identityHeaders()` into its own).
|
||||
...identityHeaders(),
|
||||
},
|
||||
...opts,
|
||||
},
|
||||
@@ -372,8 +382,8 @@ export function uploadReq<T = unknown>(
|
||||
const requestId = newRequestId();
|
||||
const started = performance.now();
|
||||
xhr.setRequestHeader(REQUEST_ID_HEADER, requestId);
|
||||
const active = getActiveAccount();
|
||||
if (active != null) xhr.setRequestHeader(ACTIVE_ACCOUNT_HEADER, String(active));
|
||||
for (const [name, value] of Object.entries(identityHeaders()))
|
||||
xhr.setRequestHeader(name, value);
|
||||
if (cfg.onProgress) {
|
||||
xhr.upload.onprogress = (e) => {
|
||||
if (e.lengthComputable) cfg.onProgress?.(Math.round((e.loaded / e.total) * 100));
|
||||
|
||||
Reference in New Issue
Block a user