feat(cms): Sentry server instrumentation + withSentryConfig + R38 PII test
Adds apps/cms/instrumentation.ts (server-only — Payload admin client DSN is out-of-scope per spec §8). Wraps the Payload-wrapped next config with withSentryConfig. Adds the R38 PII scrubber test. Required adding @repo/core-shared as a direct dep of cms (was only transitive before). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
20
apps/cms/instrumentation.ts
Normal file
20
apps/cms/instrumentation.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
// apps/cms/instrumentation.ts
|
||||
// CMS is server-only (Payload admin UI). No instrumentation-client.ts here —
|
||||
// Payload admin UI bundling is opinionated and the public DSN flow is
|
||||
// out-of-scope per spec §8.
|
||||
|
||||
export async function register() {
|
||||
if (
|
||||
process.env["NEXT_RUNTIME"] === "nodejs" ||
|
||||
process.env["NEXT_RUNTIME"] === "edge"
|
||||
) {
|
||||
const { initSentryServer } = await import(
|
||||
"@repo/core-shared/instrumentation/sentry/init-server"
|
||||
);
|
||||
initSentryServer({
|
||||
dsn: process.env["CMS_SENTRY_DSN"],
|
||||
app: "cms",
|
||||
release: process.env["VERCEL_GIT_COMMIT_SHA"],
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,14 @@
|
||||
import { withPayload } from "@payloadcms/next/withPayload";
|
||||
import { withSentryConfig } from "@sentry/nextjs";
|
||||
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {};
|
||||
|
||||
export default withPayload(nextConfig);
|
||||
export default withSentryConfig(withPayload(nextConfig), {
|
||||
silent: process.env.CI !== "true",
|
||||
authToken: process.env.SENTRY_AUTH_TOKEN,
|
||||
org: process.env.SENTRY_ORG,
|
||||
project: process.env.SENTRY_PROJECT_CMS,
|
||||
hideSourceMaps: true,
|
||||
disableLogger: true,
|
||||
});
|
||||
|
||||
@@ -16,6 +16,8 @@
|
||||
"@payloadcms/richtext-lexical": "^3.14.0",
|
||||
"@payloadcms/ui": "^3.14.0",
|
||||
"@repo/core-cms": "workspace:*",
|
||||
"@repo/core-shared": "workspace:*",
|
||||
"@sentry/nextjs": "^10.51.0",
|
||||
"next": "^15.3.0",
|
||||
"payload": "^3.14.0",
|
||||
"react": "^19.0.0",
|
||||
|
||||
56
apps/cms/src/__tests__/sentry-pii-scrubber.test.ts
Normal file
56
apps/cms/src/__tests__/sentry-pii-scrubber.test.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import {
|
||||
beforeSend,
|
||||
beforeSendTransaction,
|
||||
} from "@repo/core-shared/instrumentation/sentry/scrub";
|
||||
|
||||
describe("R38 — apps/cms PII scrubber", () => {
|
||||
it("strips email/password/cookie/auth/IP from event payload", () => {
|
||||
const event = {
|
||||
extra: {
|
||||
userEmail: "alice@example.com",
|
||||
password: "p4$$w0rd",
|
||||
ipAddress: "192.168.1.10",
|
||||
note: "request from 10.0.0.1",
|
||||
},
|
||||
request: {
|
||||
headers: {
|
||||
Authorization: "Bearer secret",
|
||||
"Set-Cookie": "session=abc",
|
||||
"User-Agent": "Mozilla",
|
||||
},
|
||||
},
|
||||
} as Parameters<typeof beforeSend>[0];
|
||||
const result = beforeSend(event, {}) as {
|
||||
extra: Record<string, string>;
|
||||
request: { headers: Record<string, string> };
|
||||
};
|
||||
expect(result.extra["userEmail"]).toBe("[redacted]");
|
||||
expect(result.extra["password"]).toBe("[redacted]");
|
||||
expect(result.extra["ipAddress"]).toBe("[redacted]");
|
||||
expect(result.extra["note"]).toContain("[redacted-ip]");
|
||||
expect(result.request.headers["Authorization"]).toBe("[redacted]");
|
||||
expect(result.request.headers["Set-Cookie"]).toBe("[redacted]");
|
||||
expect(result.request.headers["User-Agent"]).toBe("Mozilla");
|
||||
});
|
||||
|
||||
it("strips ?token / ?email / ?password / ?secret / ?signature from URLs", () => {
|
||||
const event = {
|
||||
request: {
|
||||
url: "https://app/api/x?token=abc&email=a@b.c&password=p&secret=z&signature=s&safe=1",
|
||||
},
|
||||
transaction: "/foo?accessToken=t",
|
||||
} as Parameters<typeof beforeSendTransaction>[0];
|
||||
const result = beforeSendTransaction(event, {}) as {
|
||||
request: { url: string };
|
||||
transaction: string;
|
||||
};
|
||||
const url = decodeURIComponent(result.request.url);
|
||||
const txn = decodeURIComponent(result.transaction);
|
||||
for (const key of ["token", "email", "password", "secret", "signature"]) {
|
||||
expect(url).toContain(`${key}=[redacted]`);
|
||||
}
|
||||
expect(url).toContain("safe=1");
|
||||
expect(txn).toContain("accessToken=[redacted]");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user