Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/added-nps-relay-request-logging.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- Emit one bounded structured log line per NPS relay request (route, method, status, duration) so rate-limit and auth rejections are visible in Netlify function logs.
22 changes: 21 additions & 1 deletion netlify/nps-relay/relay.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
import { readFileSync } from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { beforeEach, describe, expect, it } from "vitest";
import { beforeEach, describe, expect, it, vi } from "vitest";
import {
ACK_PATH,
CAS_MAX_ATTEMPTS,
Expand Down Expand Up @@ -989,3 +989,23 @@ describe("Netlify function wiring", () => {
expect(fn).not.toContain("INSTALL_TOKENS");
});
});

describe("request logging", () => {
it("emits one bounded log line without request data", async () => {
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
const res = await handleRelayRequest(
new Request("https://x.test/api/nps/secret-id", { method: "BREW" }),
deps()
);
const rec = JSON.parse(String(warn.mock.calls[0][0]));
warn.mockRestore();
expect(res.status).toBe(404);
expect(rec).toMatchObject({
message: "nps relay request",
route: "unknown",
method: "OTHER",
status: 404,
});
expect(JSON.stringify(rec)).not.toContain("secret-id");
});
});
30 changes: 29 additions & 1 deletion netlify/nps-relay/relay.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ import {
timingSafeEqual,
verify as cryptoVerify,
} from "node:crypto";
import { logger } from "../../src/lib/logger";

// ── Constants ────────────────────────────────────────────────────────

Expand Down Expand Up @@ -924,10 +925,37 @@ async function handleAck(req: Request, deps: RelayDeps): Promise<Response> {
return json(200, { deleted: valid.length });
}

/** Routes one request. The Netlify function is a thin wrapper around this. */
const LOG_ROUTES: Record<string, string> = {
[RELAY_BASE_PATH]: "nps",
[REGISTER_PATH]: "nps-register",
[PENDING_PATH]: "nps-pending",
[ACK_PATH]: "nps-ack",
};

/**
* Routes one request and emits one bounded structured log line (route from a
* fixed set, method, status, duration; no paths, headers, IPs or bodies).
* The Netlify function is a thin wrapper around this.
*/
export async function handleRelayRequest(
req: Request,
deps: RelayDeps
): Promise<Response> {
const started = performance.now();
const res = await routeRelayRequest(req, deps);
const path = new URL(req.url).pathname.replace(/\/+$/, "");
logger.info("nps relay request", {
route: LOG_ROUTES[path] ?? "unknown",
method: ["GET", "POST"].includes(req.method) ? req.method : "OTHER",
status: res.status,
durationMs: Math.round(performance.now() - started),
});
return res;
}

async function routeRelayRequest(
req: Request,
deps: RelayDeps
): Promise<Response> {
const now = (deps.now ?? Date.now)();
const path = new URL(req.url).pathname.replace(/\/+$/, "");
Expand Down
Loading