feat(web-next): Sentry instrumentation hooks + withSentryConfig + R38 PII test
Adds apps/web-next/instrumentation.ts (server) and instrumentation-client.ts (browser) hooks, wraps next.config.mjs with withSentryConfig (R52), and adds the R38 per-app PII scrubber smoke test. Spec deviation: extend PII_KEY_SUBSTRINGS with "ipaddress" so keys like ipAddress trigger key-level redaction (tighter posture than the spec's substring list; existing scrub.test.ts still passes). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
10
apps/web-next/instrumentation-client.ts
Normal file
10
apps/web-next/instrumentation-client.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
// apps/web-next/instrumentation-client.ts
|
||||||
|
// Next.js 15+ browser hook: runs in the client bundle on app start.
|
||||||
|
|
||||||
|
import { initSentryClient } from "@repo/core-shared/instrumentation/sentry/init-client";
|
||||||
|
|
||||||
|
initSentryClient({
|
||||||
|
dsn: process.env["NEXT_PUBLIC_WEB_NEXT_SENTRY_DSN"],
|
||||||
|
app: "web-next",
|
||||||
|
release: process.env["NEXT_PUBLIC_VERCEL_GIT_COMMIT_SHA"],
|
||||||
|
});
|
||||||
19
apps/web-next/instrumentation.ts
Normal file
19
apps/web-next/instrumentation.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
// apps/web-next/instrumentation.ts
|
||||||
|
// Next.js convention: this module runs once on server boot.
|
||||||
|
// Delegates to the centralized init helper in core-shared.
|
||||||
|
|
||||||
|
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["WEB_NEXT_SENTRY_DSN"],
|
||||||
|
app: "web-next",
|
||||||
|
release: process.env["VERCEL_GIT_COMMIT_SHA"],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { withSentryConfig } from "@sentry/nextjs";
|
||||||
|
|
||||||
/** @type {import('next').NextConfig} */
|
/** @type {import('next').NextConfig} */
|
||||||
const nextConfig = {
|
const nextConfig = {
|
||||||
transpilePackages: [
|
transpilePackages: [
|
||||||
@@ -14,4 +16,12 @@ const nextConfig = {
|
|||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
export default nextConfig;
|
export default withSentryConfig(nextConfig, {
|
||||||
|
// R52 — token is build-time only; CI sets SENTRY_AUTH_TOKEN
|
||||||
|
silent: process.env.CI !== "true",
|
||||||
|
authToken: process.env.SENTRY_AUTH_TOKEN,
|
||||||
|
org: process.env.SENTRY_ORG,
|
||||||
|
project: process.env.SENTRY_PROJECT_WEB_NEXT,
|
||||||
|
hideSourceMaps: true,
|
||||||
|
disableLogger: true,
|
||||||
|
});
|
||||||
|
|||||||
@@ -23,6 +23,7 @@
|
|||||||
"@repo/marketing-pages": "workspace:*",
|
"@repo/marketing-pages": "workspace:*",
|
||||||
"@repo/media": "workspace:*",
|
"@repo/media": "workspace:*",
|
||||||
"@repo/navigation": "workspace:*",
|
"@repo/navigation": "workspace:*",
|
||||||
|
"@sentry/nextjs": "^10.51.0",
|
||||||
"@tanstack/react-query": "^5.66.0",
|
"@tanstack/react-query": "^5.66.0",
|
||||||
"@trpc/server": "^11.0.0",
|
"@trpc/server": "^11.0.0",
|
||||||
"inversify": "^6.2.0",
|
"inversify": "^6.2.0",
|
||||||
|
|||||||
56
apps/web-next/src/__tests__/sentry-pii-scrubber.test.ts
Normal file
56
apps/web-next/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/web-next 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]");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -9,7 +9,10 @@
|
|||||||
"./trpc/init": "./src/trpc/init.ts",
|
"./trpc/init": "./src/trpc/init.ts",
|
||||||
"./trpc/context": "./src/trpc/context.ts",
|
"./trpc/context": "./src/trpc/context.ts",
|
||||||
"./trpc/define-error-middleware": "./src/trpc/define-error-middleware.ts",
|
"./trpc/define-error-middleware": "./src/trpc/define-error-middleware.ts",
|
||||||
"./instrumentation": "./src/instrumentation/index.ts"
|
"./instrumentation": "./src/instrumentation/index.ts",
|
||||||
|
"./instrumentation/sentry/init-server": "./src/instrumentation/sentry/init-server.ts",
|
||||||
|
"./instrumentation/sentry/init-client": "./src/instrumentation/sentry/init-client.ts",
|
||||||
|
"./instrumentation/sentry/scrub": "./src/instrumentation/sentry/scrub.ts"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsc --noEmit",
|
"build": "tsc --noEmit",
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ export const PII_KEY_SUBSTRINGS = [
|
|||||||
"apikey",
|
"apikey",
|
||||||
"api_key",
|
"api_key",
|
||||||
"secret",
|
"secret",
|
||||||
|
"ipaddress",
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
// R33 — substring match on URL query-param keys (case-insensitive)
|
// R33 — substring match on URL query-param keys (case-insensitive)
|
||||||
|
|||||||
31
pnpm-lock.yaml
generated
31
pnpm-lock.yaml
generated
@@ -160,6 +160,9 @@ importers:
|
|||||||
'@repo/navigation':
|
'@repo/navigation':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../../packages/navigation
|
version: link:../../packages/navigation
|
||||||
|
'@sentry/nextjs':
|
||||||
|
specifier: ^10.51.0
|
||||||
|
version: 10.51.0(@opentelemetry/core@2.7.1(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.7.1(@opentelemetry/api@1.9.1))(next@15.5.14(@opentelemetry/api@1.9.1)(@playwright/test@1.59.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.99.0))(react@19.2.4)(webpack@5.106.2)
|
||||||
'@tanstack/react-query':
|
'@tanstack/react-query':
|
||||||
specifier: ^5.66.0
|
specifier: ^5.66.0
|
||||||
version: 5.96.2(react@19.2.4)
|
version: 5.96.2(react@19.2.4)
|
||||||
@@ -567,6 +570,9 @@ importers:
|
|||||||
'@repo/core-typescript':
|
'@repo/core-typescript':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../core-typescript
|
version: link:../core-typescript
|
||||||
|
'@sentry/nextjs':
|
||||||
|
specifier: ^10.51.0
|
||||||
|
version: 10.51.0(@opentelemetry/core@2.7.1(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.7.1(@opentelemetry/api@1.9.1))(next@16.2.2(@opentelemetry/api@1.9.1)(@playwright/test@1.59.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.99.0))(react@19.2.4)(webpack@5.106.2)
|
||||||
'@types/react':
|
'@types/react':
|
||||||
specifier: ^19.0.0
|
specifier: ^19.0.0
|
||||||
version: 19.2.14
|
version: 19.2.14
|
||||||
@@ -9435,6 +9441,31 @@ snapshots:
|
|||||||
|
|
||||||
'@sentry/core@10.51.0': {}
|
'@sentry/core@10.51.0': {}
|
||||||
|
|
||||||
|
'@sentry/nextjs@10.51.0(@opentelemetry/core@2.7.1(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.7.1(@opentelemetry/api@1.9.1))(next@15.5.14(@opentelemetry/api@1.9.1)(@playwright/test@1.59.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.99.0))(react@19.2.4)(webpack@5.106.2)':
|
||||||
|
dependencies:
|
||||||
|
'@opentelemetry/api': 1.9.1
|
||||||
|
'@opentelemetry/semantic-conventions': 1.40.0
|
||||||
|
'@rollup/plugin-commonjs': 28.0.1(rollup@4.60.1)
|
||||||
|
'@sentry-internal/browser-utils': 10.51.0
|
||||||
|
'@sentry/bundler-plugin-core': 5.2.1
|
||||||
|
'@sentry/core': 10.51.0
|
||||||
|
'@sentry/node': 10.51.0
|
||||||
|
'@sentry/opentelemetry': 10.51.0(@opentelemetry/api@1.9.1)(@opentelemetry/core@2.7.1(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.7.1(@opentelemetry/api@1.9.1))(@opentelemetry/semantic-conventions@1.40.0)
|
||||||
|
'@sentry/react': 10.51.0(react@19.2.4)
|
||||||
|
'@sentry/vercel-edge': 10.51.0
|
||||||
|
'@sentry/webpack-plugin': 5.2.1(webpack@5.106.2)
|
||||||
|
next: 15.5.14(@opentelemetry/api@1.9.1)(@playwright/test@1.59.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.99.0)
|
||||||
|
rollup: 4.60.1
|
||||||
|
stacktrace-parser: 0.1.11
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- '@opentelemetry/core'
|
||||||
|
- '@opentelemetry/exporter-trace-otlp-http'
|
||||||
|
- '@opentelemetry/sdk-trace-base'
|
||||||
|
- encoding
|
||||||
|
- react
|
||||||
|
- supports-color
|
||||||
|
- webpack
|
||||||
|
|
||||||
'@sentry/nextjs@10.51.0(@opentelemetry/core@2.7.1(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.7.1(@opentelemetry/api@1.9.1))(next@16.2.2(@opentelemetry/api@1.9.1)(@playwright/test@1.59.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.99.0))(react@19.2.4)(webpack@5.106.2)':
|
'@sentry/nextjs@10.51.0(@opentelemetry/core@2.7.1(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.7.1(@opentelemetry/api@1.9.1))(next@16.2.2(@opentelemetry/api@1.9.1)(@playwright/test@1.59.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.99.0))(react@19.2.4)(webpack@5.106.2)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@opentelemetry/api': 1.9.1
|
'@opentelemetry/api': 1.9.1
|
||||||
|
|||||||
Reference in New Issue
Block a user