fix(diagnostics): deep-review round 1 — a send that cannot race, lose or repeat itself
The send, the draft and the screenshots take the session's row lock, so an autosave or a paste during a send waits for it and then sees it, and two sends cannot open two issues. A save keeps the delivery's progress; a failed edit that shows the screenshots is finished by the retry; an answer that is not the tracker's API (a redirect, a proxy page) is refused, not a crash that loses the issue's record. Only an attempt that opened an issue counts toward the pace. Screenshots: deleting one after the send is refused, a user's storage is capped across sessions, new sessions and zip exports are paced, images are stored uncompressed in the zip. One liveness rule, one session view, one image type table. Logging: a session's elevation is tracked apart from the user's own, so the browser's own requests (no header) no longer lower the floor a session needs; Stop ends it at once. Composer: an image pasted anywhere is taken; what is still unsaved is saved on close, and "saved" is shown only when it is; the draft is held while it is sent; a failed screenshot read says so instead of reading as none; focus moves to what stays; the preview follows the text size. Pill: the step prompt and a running Stop survive the pill moving between hosts, the prompt keeps the dialog's Tab trap, the toast region lets clicks through, other tabs hear of a Start or Stop, the last seconds are sent before the cap, a capture with no frame is an error.
This commit is contained in:
@@ -1,10 +1,17 @@
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { useCallback } from "react";
|
||||
import { useIsMutating, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { api, type DebugSession, type DebugSessionLevel } from "../lib/api";
|
||||
import { notifyActionError } from "../lib/actionErrors";
|
||||
import { captureTab } from "../lib/captureTab";
|
||||
import { closeRecorderStart, nextStep, openComposer } from "../lib/debugRecorder";
|
||||
import {
|
||||
announceSessionChange,
|
||||
closeRecorderStart,
|
||||
nextStep,
|
||||
openComposer,
|
||||
setStepPrompt,
|
||||
} from "../lib/debugRecorder";
|
||||
import { flushNow, liveSession } from "../lib/diagnostics";
|
||||
import { log } from "../lib/log";
|
||||
import { notify } from "../lib/notifications";
|
||||
@@ -21,7 +28,9 @@ export function useDebugRecording() {
|
||||
const session = liveSession(me?.debug_session);
|
||||
const setSession = (next: DebugSession | null) => {
|
||||
updateMeCache(qc, (prev) => ({ ...prev, debug_session: next }));
|
||||
void qc.invalidateQueries({ queryKey: qk.debugSessions() });
|
||||
void qc.invalidateQueries({ queryKey: qk.debugSessions(), exact: true });
|
||||
announceSessionChange();
|
||||
setStepPrompt(null); // a prompt left open belongs to the session that ended
|
||||
};
|
||||
|
||||
const start = useMutation({
|
||||
@@ -34,6 +43,7 @@ export function useDebugRecording() {
|
||||
});
|
||||
|
||||
const stop = useMutation({
|
||||
mutationKey: STOP_KEY,
|
||||
mutationFn: async (id: string) => {
|
||||
log.info("debug.stop", "recording stopped by the user");
|
||||
// The events of the last seconds before Stop are the ones closest to what went wrong. Sent
|
||||
@@ -50,6 +60,7 @@ export function useDebugRecording() {
|
||||
});
|
||||
|
||||
const shoot = useMutation({
|
||||
mutationKey: SHOOT_KEY,
|
||||
mutationFn: async ({ id, image }: { id: string; image: Blob }) =>
|
||||
api.addDebugScreenshot(id, image),
|
||||
meta: { failTitle: "recorder.pill.shotFailed" },
|
||||
@@ -71,14 +82,23 @@ export function useDebugRecording() {
|
||||
},
|
||||
});
|
||||
|
||||
// In flight anywhere, not only through this hook's own observer: the pill remounts when it moves
|
||||
// to another toast host, and a Stop still running must keep its button busy there.
|
||||
const stopping = useIsMutating({ mutationKey: STOP_KEY }) > 0;
|
||||
const shooting = useIsMutating({ mutationKey: SHOOT_KEY }) > 0;
|
||||
|
||||
/** Mark the next step; `label` is what the user says they just did (may be empty). */
|
||||
const markStep = (label: string) => {
|
||||
if (!session) return;
|
||||
const n = nextStep(session.id);
|
||||
const text = label.trim();
|
||||
log.info("debug.step", text || `step ${n}`, { n, ...(text ? { label: text } : {}) });
|
||||
notify({ level: "info", message: t("recorder.pill.stepMarked", { n }) });
|
||||
};
|
||||
const sessionId = session?.id;
|
||||
const markStep = useCallback(
|
||||
(label: string) => {
|
||||
if (!sessionId) return;
|
||||
const n = nextStep(sessionId);
|
||||
const text = label.trim();
|
||||
log.info("debug.step", text || `step ${n}`, { n, ...(text ? { label: text } : {}) });
|
||||
notify({ level: "info", message: t("recorder.pill.stepMarked", { n }) });
|
||||
},
|
||||
[sessionId, t],
|
||||
);
|
||||
|
||||
/** Capture this tab and attach it. `hide` runs around the capture so the pill is not in the
|
||||
* frame. Nothing happens when the user cancels the browser's picker. */
|
||||
@@ -98,5 +118,8 @@ export function useDebugRecording() {
|
||||
if (image) shoot.mutate({ id: session.id, image });
|
||||
};
|
||||
|
||||
return { session, start, stop, shoot, markStep, screenshot };
|
||||
return { session, start, stop, stopping, shoot, shooting, markStep, screenshot };
|
||||
}
|
||||
|
||||
const STOP_KEY = ["debug-session", "stop"];
|
||||
const SHOOT_KEY = ["debug-session", "screenshot"];
|
||||
|
||||
Reference in New Issue
Block a user