fix: resolve 20 Sentry issues and fix navbar test flakiness

Sentry fixes:
- Only send 5xx errors to Sentry (was sending 4xx rate-limit, media type errors)
- Encode non-ASCII chars in X-Output-Filename header (encodeURIComponent)
- Handle FK constraint failures gracefully in file upload, pipeline save, API keys
- Harden getDirSize against ENOENT race on readdirSync

Test fixes:
- Wrap navbar test renders in act() to flush async useEffect state updates
- Add useEffect cleanup to navbar to prevent state updates on unmounted component
- Fixes timeout when running in full test suite
This commit is contained in:
SnapOtter
2026-05-01 19:01:05 +08:00
parent ff8dcf63c7
commit 66211ed6a7
9 changed files with 128 additions and 87 deletions
+3 -1
View File
@@ -100,7 +100,9 @@ app.setErrorHandler((error: Error & { statusCode?: number }, request, reply) =>
{ err: error, url: request.url, method: request.method },
"Unhandled request error",
);
captureException(error, request);
if (statusCode >= 500) {
captureException(error, request);
}
const isProduction = process.env.NODE_ENV === "production";
reply.status(statusCode).send({
error: statusCode >= 500 ? "Internal server error" : error.message,
+15 -11
View File
@@ -72,17 +72,21 @@ export async function apiKeyRoutes(app: FastifyInstance): Promise<void> {
const keyPrefix = computeKeyPrefix(rawKey);
const id = randomUUID();
db.insert(schema.apiKeys)
.values({
id,
userId: user.id,
keyHash,
keyPrefix,
name,
permissions: scopedPermissions ? JSON.stringify(scopedPermissions) : null,
expiresAt,
})
.run();
try {
db.insert(schema.apiKeys)
.values({
id,
userId: user.id,
keyHash,
keyPrefix,
name,
permissions: scopedPermissions ? JSON.stringify(scopedPermissions) : null,
expiresAt,
})
.run();
} catch {
return reply.status(409).send({ error: "Failed to create API key" });
}
auditLog(request.log, "API_KEY_CREATED", { userId: user.id, keyId: id, keyName: name });
+7 -2
View File
@@ -9,7 +9,7 @@
import { spawn } from "node:child_process";
import crypto from "node:crypto";
import { existsSync, readdirSync, readFileSync, statSync, unlinkSync } from "node:fs";
import { type Dirent, existsSync, readdirSync, readFileSync, statSync, unlinkSync } from "node:fs";
import { join } from "node:path";
import { shutdownDispatcher } from "@snapotter/ai";
import { ANALYTICS_EVENTS, FEATURE_BUNDLES } from "@snapotter/shared";
@@ -67,7 +67,12 @@ function getDirSize(dirPath: string): number {
if (!existsSync(dirPath)) return 0;
let total = 0;
const entries = readdirSync(dirPath, { withFileTypes: true });
let entries: Dirent[];
try {
entries = readdirSync(dirPath, { withFileTypes: true });
} catch {
return 0;
}
for (const entry of entries) {
const fullPath = join(dirPath, entry.name);
if (entry.isDirectory()) {
+13 -9
View File
@@ -329,15 +329,19 @@ export async function registerPipelineRoutes(app: FastifyInstance): Promise<void
const id = randomUUID();
db.insert(schema.pipelines)
.values({
id,
userId: user.id,
name,
description: description ?? null,
steps: JSON.stringify(steps),
})
.run();
try {
db.insert(schema.pipelines)
.values({
id,
userId: user.id,
name,
description: description ?? null,
steps: JSON.stringify(steps),
})
.run();
} catch {
return reply.status(409).send({ error: "Failed to save pipeline" });
}
return reply.status(201).send({
id,
@@ -149,7 +149,7 @@ export function registerOptimizeForWeb(app: FastifyInstance) {
reply.header("Content-Type", result.contentType);
reply.header("X-Original-Size", String(fileBuffer.length));
reply.header("X-Processed-Size", String(result.buffer.length));
reply.header("X-Output-Filename", result.filename);
reply.header("X-Output-Filename", encodeURIComponent(result.filename));
return reply.send(result.buffer);
} catch (err) {
const message = err instanceof Error ? err.message : "Preview processing failed";
+38 -30
View File
@@ -198,21 +198,25 @@ export async function userFileRoutes(app: FastifyInstance): Promise<void> {
// Create DB record
const id = randomUUID();
db.insert(schema.userFiles)
.values({
id,
userId,
originalName: safeName,
storedName,
mimeType,
size: safeBuffer.length,
width: validation.width,
height: validation.height,
version: 1,
parentId: null,
toolChain: null,
})
.run();
try {
db.insert(schema.userFiles)
.values({
id,
userId,
originalName: safeName,
storedName,
mimeType,
size: safeBuffer.length,
width: validation.width,
height: validation.height,
version: 1,
parentId: null,
toolChain: null,
})
.run();
} catch {
return reply.status(409).send({ error: "Failed to save file record" });
}
const row = db.select().from(schema.userFiles).where(eq(schema.userFiles.id, id)).get();
@@ -555,21 +559,25 @@ export async function userFileRoutes(app: FastifyInstance): Promise<void> {
// Create DB record
const id = randomUUID();
db.insert(schema.userFiles)
.values({
id,
userId,
originalName: resultName,
storedName,
mimeType,
size: safeResultBuffer.length,
width: validation.width,
height: validation.height,
version: nextVersion,
parentId,
toolChain: JSON.stringify(newChain),
})
.run();
try {
db.insert(schema.userFiles)
.values({
id,
userId,
originalName: resultName,
storedName,
mimeType,
size: safeResultBuffer.length,
width: validation.width,
height: validation.height,
version: nextVersion,
parentId,
toolChain: JSON.stringify(newChain),
})
.run();
} catch {
return reply.status(409).send({ error: "Failed to save result record" });
}
const row = db.select().from(schema.userFiles).where(eq(schema.userFiles.id, id)).get();