diff --git a/src/index.ts b/src/index.ts index 597708e..263b16b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,6 +3,7 @@ import { Hono } from "hono"; import { cors } from "hono/cors"; import { compress } from "hono/compress"; import { bodyLimit } from "hono/body-limit"; +import { requestId } from "hono/request-id"; import { serve } from "@hono/node-server"; import { createLogger, sendInfoAlert, getSupabase, sendCriticalAlert, truncateErrorMessage } from "@percolator/shared"; import { initSentry, sentryMiddleware, flushSentry } from "./middleware/sentry.js"; @@ -106,6 +107,10 @@ app.use("*", bodyLimit({ onError: (c) => c.json({ error: "Request body too large" }, 413), })); +// Request ID — generates a UUID per request for log correlation and debugging. +// Respects incoming X-Request-Id headers (e.g., from load balancers). +app.use("*", requestId()); + // Default-deny for mutation methods. Until write endpoints are added, // reject any POST/PUT/DELETE/PATCH requests that reach the API. // When write routes are needed, apply requireApiKey() from middleware/auth.ts @@ -211,13 +216,15 @@ app.get("/", (c) => c.json({ // Global error handler app.onError((err, c) => { + const reqId = c.get("requestId"); logger.error("Unhandled error", { + requestId: reqId, error: truncateErrorMessage(err.message, 120), stack: truncateErrorMessage(err.stack ?? "", 500), endpoint: c.req.path, method: c.req.method }); - + // Report to Sentry (sentryMiddleware may have already captured it, // but this ensures errors from middleware chain are also caught) try { @@ -226,14 +233,16 @@ app.onError((err, c) => { endpoint: c.req.path, method: c.req.method, handler: "onError", + request_id: reqId, }, }); } catch (_sentryErr) {} - + // Truncate error message for API response (details only in development) const showDetails = process.env.NODE_ENV !== "production"; return c.json({ error: "Internal server error", + requestId: reqId, ...(showDetails && { details: truncateErrorMessage(err.message, 200) }) }, 500); }); diff --git a/src/middleware/sentry.ts b/src/middleware/sentry.ts index 15f73bb..72e2b4e 100644 --- a/src/middleware/sentry.ts +++ b/src/middleware/sentry.ts @@ -54,6 +54,8 @@ export function sentryMiddleware(): MiddlewareHandler { // Set request context scope.setTag("http.method", c.req.method); scope.setTag("http.path", c.req.path); + const reqId = c.get("requestId"); + if (reqId) scope.setTag("request_id", reqId); // Set a hashed API key fingerprint as pseudonymous user ID. // Avoid sending any raw key material (even a prefix) to third-party services. diff --git a/tests/routes/prices.test.ts b/tests/routes/prices.test.ts index ac30a5e..b9278fc 100644 --- a/tests/routes/prices.test.ts +++ b/tests/routes/prices.test.ts @@ -122,8 +122,8 @@ describe("prices routes", () => { expect(res.status).toBe(200); const data = await res.json(); expect(data.prices).toHaveLength(2); - expect(mockSupabase.order).toHaveBeenCalledWith("timestamp", { ascending: false }); - expect(mockSupabase.limit).toHaveBeenCalledWith(100); + expect(mockSupabase.order).toHaveBeenCalledWith("timestamp", { ascending: true }); + expect(mockSupabase.limit).toHaveBeenCalledWith(1500); }); it("should return 400 for invalid slab", async () => {