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:
@@ -34,7 +34,11 @@ beforeEach(() => {
|
||||
let current: typeof import("./diagnostics") | null = null;
|
||||
afterEach(() => {
|
||||
// A module copy left streaming would still hear `pagehide` in the next test.
|
||||
current?.applyDiagnostics({ level: "INFO", until: null, retention_days: 7 }, () => {});
|
||||
current?.applyDiagnostics(
|
||||
{ level: "INFO", until: null, retention_days: 7, session_minutes: 60 },
|
||||
null,
|
||||
() => {},
|
||||
);
|
||||
current = null;
|
||||
vi.useRealTimers();
|
||||
});
|
||||
@@ -51,6 +55,7 @@ const raised = (level: "DEBUG" | "TRACE", hours = 24) => ({
|
||||
level,
|
||||
until: new Date(Date.now() + hours * 3_600_000).toISOString(),
|
||||
retention_days: 7,
|
||||
session_minutes: 60,
|
||||
});
|
||||
const codes = (batch: unknown[] | undefined) =>
|
||||
(batch ?? []).map((e) => (e as { event: string }).event);
|
||||
@@ -58,7 +63,11 @@ const codes = (batch: unknown[] | undefined) =>
|
||||
describe("diagnostics level", () => {
|
||||
it("the base level at boot sets INFO, streams nothing and logs nothing", async () => {
|
||||
const d = await fresh();
|
||||
d.applyDiagnostics({ level: "INFO", until: null, retention_days: 7 }, () => {});
|
||||
d.applyDiagnostics(
|
||||
{ level: "INFO", until: null, retention_days: 7, session_minutes: 60 },
|
||||
null,
|
||||
() => {},
|
||||
);
|
||||
d.log.info("nav.view", "x");
|
||||
await vi.advanceTimersByTimeAsync(60_000);
|
||||
expect(sent).toEqual([]);
|
||||
@@ -69,7 +78,7 @@ describe("diagnostics level", () => {
|
||||
it("a raised level streams only what happens after it, without its own request lines", async () => {
|
||||
const d = await fresh();
|
||||
d.log.info("before.consent", "not sent");
|
||||
d.applyDiagnostics(raised("DEBUG"), () => {});
|
||||
d.applyDiagnostics(raised("DEBUG"), null, () => {});
|
||||
expect(d.enabled("DEBUG")).toBe(true);
|
||||
d.log.debug("ui.click", "click");
|
||||
d.log.request("INFO", "POST", { path: "/api/diagnostics/events", status: 200 }, "abcdef0123");
|
||||
@@ -82,7 +91,7 @@ describe("diagnostics level", () => {
|
||||
|
||||
it("sends in batches of at most 100, and a full batch goes without waiting", async () => {
|
||||
const d = await fresh();
|
||||
d.applyDiagnostics(raised("DEBUG"), () => {});
|
||||
d.applyDiagnostics(raised("DEBUG"), null, () => {});
|
||||
for (let i = 0; i < 250; i++) d.log.debug("ui.click", `click ${i}`);
|
||||
await vi.advanceTimersByTimeAsync(0);
|
||||
expect(sent.map((b) => b.length)).toEqual([100]);
|
||||
@@ -94,9 +103,13 @@ describe("diagnostics level", () => {
|
||||
|
||||
it("stops at once when the level is lowered, and drops what was not sent yet", async () => {
|
||||
const d = await fresh();
|
||||
d.applyDiagnostics(raised("TRACE"), () => {});
|
||||
d.applyDiagnostics(raised("TRACE"), null, () => {});
|
||||
d.log.trace("search.typed", "lofi");
|
||||
d.applyDiagnostics({ level: "INFO", until: null, retention_days: 7 }, () => {});
|
||||
d.applyDiagnostics(
|
||||
{ level: "INFO", until: null, retention_days: 7, session_minutes: 60 },
|
||||
null,
|
||||
() => {},
|
||||
);
|
||||
d.log.info("nav.view", "after");
|
||||
await vi.advanceTimersByTimeAsync(60_000);
|
||||
expect(sent).toEqual([]);
|
||||
@@ -106,7 +119,7 @@ describe("diagnostics level", () => {
|
||||
it("lapses on its own timer and tells the app to refetch", async () => {
|
||||
const d = await fresh();
|
||||
const lapsed = vi.fn();
|
||||
d.applyDiagnostics(raised("DEBUG", 1), lapsed);
|
||||
d.applyDiagnostics(raised("DEBUG", 1), null, lapsed);
|
||||
await vi.advanceTimersByTimeAsync(3_600_000 + 1);
|
||||
expect(lapsed).toHaveBeenCalledOnce();
|
||||
expect(d.enabled("DEBUG")).toBe(false);
|
||||
@@ -118,14 +131,14 @@ describe("diagnostics level", () => {
|
||||
|
||||
it("an already-expired level is the base level", async () => {
|
||||
const d = await fresh();
|
||||
d.applyDiagnostics(raised("DEBUG", -1), () => {});
|
||||
d.applyDiagnostics(raised("DEBUG", -1), null, () => {});
|
||||
expect(d.enabled("DEBUG")).toBe(false);
|
||||
});
|
||||
|
||||
it("the server saying it is off stops the stream; being told to slow down keeps the batch", async () => {
|
||||
const d = await fresh();
|
||||
const lapsed = vi.fn();
|
||||
d.applyDiagnostics(raised("DEBUG"), lapsed);
|
||||
d.applyDiagnostics(raised("DEBUG"), null, lapsed);
|
||||
answer = () => Promise.reject(new d.HttpError(429, "slow down"));
|
||||
d.log.debug("ui.click", "kept for later");
|
||||
await vi.advanceTimersByTimeAsync(5_000);
|
||||
@@ -138,7 +151,7 @@ describe("diagnostics level", () => {
|
||||
|
||||
it("after a 429 nothing is sent until the back-off is over, however full the queue", async () => {
|
||||
const d = await fresh();
|
||||
d.applyDiagnostics(raised("DEBUG"), () => {});
|
||||
d.applyDiagnostics(raised("DEBUG"), null, () => {});
|
||||
answer = () => Promise.reject(new d.HttpError(429, "slow down"));
|
||||
for (let i = 0; i < 99; i++) d.log.debug("ui.click", `click ${i}`);
|
||||
await vi.advanceTimersByTimeAsync(0);
|
||||
@@ -153,7 +166,7 @@ describe("diagnostics level", () => {
|
||||
|
||||
it("past the queue cap the oldest events go, a refused batch included", async () => {
|
||||
const d = await fresh();
|
||||
d.applyDiagnostics(raised("DEBUG"), () => {});
|
||||
d.applyDiagnostics(raised("DEBUG"), null, () => {});
|
||||
answer = () => Promise.reject(new d.HttpError(429, "slow down"));
|
||||
for (let i = 0; i < 99; i++) d.log.debug("ui.click", `click ${i}`);
|
||||
await vi.advanceTimersByTimeAsync(0);
|
||||
@@ -173,14 +186,18 @@ describe("diagnostics level", () => {
|
||||
const d = await fresh();
|
||||
let refuse: (err: unknown) => void = () => {};
|
||||
answer = () => new Promise((_, reject) => (refuse = reject));
|
||||
d.applyDiagnostics(raised("DEBUG"), () => {});
|
||||
d.applyDiagnostics(raised("DEBUG"), null, () => {});
|
||||
d.log.debug("ui.click", "under the first consent");
|
||||
await vi.advanceTimersByTimeAsync(5_000); // in flight
|
||||
d.applyDiagnostics({ level: "INFO", until: null, retention_days: 7 }, () => {});
|
||||
d.applyDiagnostics(
|
||||
{ level: "INFO", until: null, retention_days: 7, session_minutes: 60 },
|
||||
null,
|
||||
() => {},
|
||||
);
|
||||
refuse(new d.HttpError(429, "slow down"));
|
||||
await vi.advanceTimersByTimeAsync(0);
|
||||
answer = () => Promise.resolve({ ok: true, stored: 0 });
|
||||
d.applyDiagnostics(raised("TRACE"), () => {});
|
||||
d.applyDiagnostics(raised("TRACE"), null, () => {});
|
||||
await vi.advanceTimersByTimeAsync(60_000);
|
||||
const later = sent
|
||||
.slice(1)
|
||||
@@ -198,12 +215,16 @@ describe("diagnostics level", () => {
|
||||
const d = await fresh();
|
||||
let refuse: (err: unknown) => void = () => {};
|
||||
answer = () => new Promise((_, reject) => (refuse = reject));
|
||||
d.applyDiagnostics(raised("DEBUG"), () => {});
|
||||
d.applyDiagnostics(raised("DEBUG"), null, () => {});
|
||||
d.log.debug("ui.click", "under the first consent");
|
||||
await vi.advanceTimersByTimeAsync(5_000); // in flight
|
||||
d.applyDiagnostics({ level: "INFO", until: null, retention_days: 7 }, () => {});
|
||||
d.applyDiagnostics(
|
||||
{ level: "INFO", until: null, retention_days: 7, session_minutes: 60 },
|
||||
null,
|
||||
() => {},
|
||||
);
|
||||
answer = () => Promise.resolve({ ok: true, stored: 0 });
|
||||
d.applyDiagnostics(raised("TRACE"), () => {});
|
||||
d.applyDiagnostics(raised("TRACE"), null, () => {});
|
||||
refuse(new d.HttpError(429, "slow down")); // only now does the old consent's batch come back
|
||||
await vi.advanceTimersByTimeAsync(60_000);
|
||||
const later = sent
|
||||
@@ -219,12 +240,16 @@ describe("diagnostics level", () => {
|
||||
const lapsed = vi.fn();
|
||||
let refuse: (err: unknown) => void = () => {};
|
||||
answer = () => new Promise((_, reject) => (refuse = reject));
|
||||
d.applyDiagnostics(raised("DEBUG"), lapsed);
|
||||
d.applyDiagnostics(raised("DEBUG"), null, lapsed);
|
||||
d.log.debug("ui.click", "under the first consent");
|
||||
await vi.advanceTimersByTimeAsync(5_000); // in flight
|
||||
d.applyDiagnostics({ level: "INFO", until: null, retention_days: 7 }, lapsed);
|
||||
d.applyDiagnostics(
|
||||
{ level: "INFO", until: null, retention_days: 7, session_minutes: 60 },
|
||||
null,
|
||||
lapsed,
|
||||
);
|
||||
answer = () => Promise.resolve({ ok: true, stored: 0 });
|
||||
d.applyDiagnostics(raised("TRACE"), lapsed);
|
||||
d.applyDiagnostics(raised("TRACE"), null, lapsed);
|
||||
refuse(new d.HttpError(409, "off"));
|
||||
await vi.advanceTimersByTimeAsync(60_000);
|
||||
expect(lapsed).not.toHaveBeenCalled();
|
||||
@@ -238,10 +263,14 @@ describe("diagnostics level", () => {
|
||||
const lapsed = vi.fn();
|
||||
let refuse: (err: unknown) => void = () => {};
|
||||
answer = () => new Promise((_, reject) => (refuse = reject));
|
||||
d.applyDiagnostics(raised("DEBUG"), lapsed);
|
||||
d.applyDiagnostics(raised("DEBUG"), null, lapsed);
|
||||
d.log.debug("ui.click", "under the consent");
|
||||
await vi.advanceTimersByTimeAsync(5_000); // in flight
|
||||
d.applyDiagnostics({ level: "INFO", until: null, retention_days: 7 }, lapsed);
|
||||
d.applyDiagnostics(
|
||||
{ level: "INFO", until: null, retention_days: 7, session_minutes: 60 },
|
||||
null,
|
||||
lapsed,
|
||||
);
|
||||
refuse(new d.HttpError(409, "off"));
|
||||
await vi.advanceTimersByTimeAsync(0);
|
||||
expect(lapsed).not.toHaveBeenCalled();
|
||||
@@ -255,7 +284,7 @@ describe("diagnostics level", () => {
|
||||
const d = await fresh();
|
||||
let refuse: (err: unknown) => void = () => {};
|
||||
answer = () => new Promise((_, reject) => (refuse = reject));
|
||||
d.applyDiagnostics(raised("DEBUG"), () => {});
|
||||
d.applyDiagnostics(raised("DEBUG"), null, () => {});
|
||||
for (let i = 0; i < 99; i++) d.log.debug("ui.click", `old ${i}`);
|
||||
await vi.advanceTimersByTimeAsync(0); // the 100 oldest are in flight, the queue is empty
|
||||
for (let i = 0; i < 1_000; i++) d.log.debug("ui.click", `new ${i}`);
|
||||
@@ -273,7 +302,7 @@ describe("diagnostics level", () => {
|
||||
|
||||
it("a closing tab sends what is still waiting with a keepalive request", async () => {
|
||||
const d = await fresh();
|
||||
d.applyDiagnostics(raised("DEBUG"), () => {});
|
||||
d.applyDiagnostics(raised("DEBUG"), null, () => {});
|
||||
d.log.debug("ui.click", "last click");
|
||||
window.dispatchEvent(new Event("pagehide"));
|
||||
expect(codes(unloaded[0])).toEqual(["diagnostics.level", "ui.click"]);
|
||||
|
||||
Reference in New Issue
Block a user