fix(web): preserve coalesced tile error logs
This commit is contained in:
parent
328f6e00bf
commit
0d27920a78
2 changed files with 119 additions and 10 deletions
|
|
@ -389,6 +389,62 @@ describe("dashboard API route", () => {
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("logs coalesced metadata when shared tile requests fail", async () => {
|
||||||
|
const cache = createDashboardTileCache();
|
||||||
|
let rejectLoad: ((error: Error) => void) | undefined;
|
||||||
|
const load = vi.fn(() =>
|
||||||
|
new Promise<never>((_resolve, reject) => {
|
||||||
|
rejectLoad = reject;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const first = cache.resolve("tile-a", 30_000, 1_000, load);
|
||||||
|
const second = cache.resolve("tile-a", 30_000, 1_000, load);
|
||||||
|
|
||||||
|
await Promise.resolve();
|
||||||
|
expect(load).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
|
rejectLoad?.(new TypeError("upstream failed"));
|
||||||
|
await expect(first).rejects.toThrow("upstream failed");
|
||||||
|
await expect(second).rejects.toMatchObject({
|
||||||
|
cache: "miss",
|
||||||
|
coalesced: true,
|
||||||
|
cause: expect.any(TypeError),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("logs coalesced metadata for failed tile cache resolution", async () => {
|
||||||
|
const logs: DashboardTileResolutionLogEvent[] = [];
|
||||||
|
const tile = { kind: "service", groupId: "essentials", id: "vaultwarden" } as const;
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
loadDashboardTileResponse(tile, {
|
||||||
|
logTileResolution: (event) => logs.push(event),
|
||||||
|
refreshSeedDocument: true,
|
||||||
|
seedIfEmpty: true,
|
||||||
|
tileCache: {
|
||||||
|
async resolve() {
|
||||||
|
throw {
|
||||||
|
cache: "miss",
|
||||||
|
cause: new TypeError("coalesced cache failed"),
|
||||||
|
coalesced: true,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
).rejects.toThrow("coalesced cache failed");
|
||||||
|
|
||||||
|
expect(logs).toEqual([
|
||||||
|
expect.objectContaining({
|
||||||
|
cache: "miss",
|
||||||
|
coalesced: true,
|
||||||
|
errorCategory: "TypeError",
|
||||||
|
status: "error",
|
||||||
|
tileKey: dashboardTileCacheKey(tile),
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
test("logs error categories for failed tile cache resolution", async () => {
|
test("logs error categories for failed tile cache resolution", async () => {
|
||||||
const logs: DashboardTileResolutionLogEvent[] = [];
|
const logs: DashboardTileResolutionLogEvent[] = [];
|
||||||
const tile = { kind: "service", groupId: "essentials", id: "vaultwarden" } as const;
|
const tile = { kind: "service", groupId: "essentials", id: "vaultwarden" } as const;
|
||||||
|
|
|
||||||
|
|
@ -77,11 +77,18 @@ export function createDashboardTileCache(): DashboardTileCache {
|
||||||
|
|
||||||
const active = inFlight.get(key);
|
const active = inFlight.get(key);
|
||||||
if (active) {
|
if (active) {
|
||||||
return active.then((response) => ({
|
return active
|
||||||
cache: "miss",
|
.then((response) => ({
|
||||||
coalesced: true,
|
cache: "miss" as const,
|
||||||
response,
|
coalesced: true,
|
||||||
}));
|
response,
|
||||||
|
}))
|
||||||
|
.catch((error) => {
|
||||||
|
throw new DashboardTileCacheResolutionError(error, {
|
||||||
|
cache: "miss",
|
||||||
|
coalesced: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const request = load()
|
const request = load()
|
||||||
|
|
@ -101,7 +108,7 @@ export function createDashboardTileCache(): DashboardTileCache {
|
||||||
|
|
||||||
inFlight.set(key, request);
|
inFlight.set(key, request);
|
||||||
return request.then((response) => ({
|
return request.then((response) => ({
|
||||||
cache: "miss",
|
cache: "miss" as const,
|
||||||
coalesced: false,
|
coalesced: false,
|
||||||
response,
|
response,
|
||||||
}));
|
}));
|
||||||
|
|
@ -216,15 +223,16 @@ export async function loadDashboardTileResponse(
|
||||||
});
|
});
|
||||||
return result.response;
|
return result.response;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
const cacheError = dashboardTileCacheResolutionError(error);
|
||||||
logDashboardTileResolution(options, {
|
logDashboardTileResolution(options, {
|
||||||
cache: "miss",
|
cache: cacheError?.cache ?? "miss",
|
||||||
coalesced: false,
|
coalesced: cacheError?.coalesced ?? false,
|
||||||
durationMs: elapsedDashboardTileMs(startedAt, options),
|
durationMs: elapsedDashboardTileMs(startedAt, options),
|
||||||
errorCategory: dashboardTileErrorCategory(error),
|
errorCategory: dashboardTileErrorCategory(cacheError?.cause ?? error),
|
||||||
status: "error",
|
status: "error",
|
||||||
tileKey,
|
tileKey,
|
||||||
});
|
});
|
||||||
throw error;
|
throw cacheError?.cause ?? error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -288,6 +296,51 @@ function dashboardTileErrorCategory(error: unknown): string {
|
||||||
return "unknown";
|
return "unknown";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class DashboardTileCacheResolutionError extends Error {
|
||||||
|
readonly cache: "hit" | "miss";
|
||||||
|
readonly coalesced: boolean;
|
||||||
|
override readonly cause: unknown;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
cause: unknown,
|
||||||
|
metadata: Pick<DashboardTileCacheResult, "cache" | "coalesced">,
|
||||||
|
) {
|
||||||
|
super("Dashboard tile cache resolution failed");
|
||||||
|
this.name = "DashboardTileCacheResolutionError";
|
||||||
|
this.cause = cause;
|
||||||
|
this.cache = metadata.cache;
|
||||||
|
this.coalesced = metadata.coalesced;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function dashboardTileCacheResolutionError(
|
||||||
|
error: unknown,
|
||||||
|
): DashboardTileCacheResolutionFailure | null {
|
||||||
|
if (
|
||||||
|
typeof error === "object" &&
|
||||||
|
error !== null &&
|
||||||
|
"cache" in error &&
|
||||||
|
(error.cache === "hit" || error.cache === "miss") &&
|
||||||
|
"coalesced" in error &&
|
||||||
|
typeof error.coalesced === "boolean" &&
|
||||||
|
"cause" in error
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
cache: error.cache,
|
||||||
|
cause: error.cause,
|
||||||
|
coalesced: error.coalesced,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DashboardTileCacheResolutionFailure {
|
||||||
|
cache: "hit" | "miss";
|
||||||
|
cause: unknown;
|
||||||
|
coalesced: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
function dashboardTileTtlMs(
|
function dashboardTileTtlMs(
|
||||||
document: DashboardDocument,
|
document: DashboardDocument,
|
||||||
tile: DashboardTileReference,
|
tile: DashboardTileReference,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue