From f911892d0b25b1c119435909d7a3ba91e6da01f9 Mon Sep 17 00:00:00 2001 From: Danijel Martinek Date: Thu, 7 May 2026 20:13:39 +0200 Subject: [PATCH] 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) --- apps/web-next/instrumentation-client.ts | 10 ++++ apps/web-next/instrumentation.ts | 19 +++++++ apps/web-next/next.config.mjs | 12 +++- apps/web-next/package.json | 1 + .../src/__tests__/sentry-pii-scrubber.test.ts | 56 +++++++++++++++++++ packages/core-shared/package.json | 5 +- .../src/instrumentation/sentry/pii-fields.ts | 1 + pnpm-lock.yaml | 31 ++++++++++ 8 files changed, 133 insertions(+), 2 deletions(-) create mode 100644 apps/web-next/instrumentation-client.ts create mode 100644 apps/web-next/instrumentation.ts create mode 100644 apps/web-next/src/__tests__/sentry-pii-scrubber.test.ts diff --git a/apps/web-next/instrumentation-client.ts b/apps/web-next/instrumentation-client.ts new file mode 100644 index 0000000..7e529e8 --- /dev/null +++ b/apps/web-next/instrumentation-client.ts @@ -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"], +}); diff --git a/apps/web-next/instrumentation.ts b/apps/web-next/instrumentation.ts new file mode 100644 index 0000000..96d3ff1 --- /dev/null +++ b/apps/web-next/instrumentation.ts @@ -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"], + }); + } +} diff --git a/apps/web-next/next.config.mjs b/apps/web-next/next.config.mjs index 90cb312..eaa8607 100644 --- a/apps/web-next/next.config.mjs +++ b/apps/web-next/next.config.mjs @@ -1,3 +1,5 @@ +import { withSentryConfig } from "@sentry/nextjs"; + /** @type {import('next').NextConfig} */ const nextConfig = { 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, +}); diff --git a/apps/web-next/package.json b/apps/web-next/package.json index 14cebc0..b1162dd 100644 --- a/apps/web-next/package.json +++ b/apps/web-next/package.json @@ -23,6 +23,7 @@ "@repo/marketing-pages": "workspace:*", "@repo/media": "workspace:*", "@repo/navigation": "workspace:*", + "@sentry/nextjs": "^10.51.0", "@tanstack/react-query": "^5.66.0", "@trpc/server": "^11.0.0", "inversify": "^6.2.0", diff --git a/apps/web-next/src/__tests__/sentry-pii-scrubber.test.ts b/apps/web-next/src/__tests__/sentry-pii-scrubber.test.ts new file mode 100644 index 0000000..40b562b --- /dev/null +++ b/apps/web-next/src/__tests__/sentry-pii-scrubber.test.ts @@ -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[0]; + const result = beforeSend(event, {}) as { + extra: Record; + request: { headers: Record }; + }; + 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[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]"); + }); +}); diff --git a/packages/core-shared/package.json b/packages/core-shared/package.json index 8dab678..f34e6e4 100644 --- a/packages/core-shared/package.json +++ b/packages/core-shared/package.json @@ -9,7 +9,10 @@ "./trpc/init": "./src/trpc/init.ts", "./trpc/context": "./src/trpc/context.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": { "build": "tsc --noEmit", diff --git a/packages/core-shared/src/instrumentation/sentry/pii-fields.ts b/packages/core-shared/src/instrumentation/sentry/pii-fields.ts index 897b830..3a1d28b 100644 --- a/packages/core-shared/src/instrumentation/sentry/pii-fields.ts +++ b/packages/core-shared/src/instrumentation/sentry/pii-fields.ts @@ -12,6 +12,7 @@ export const PII_KEY_SUBSTRINGS = [ "apikey", "api_key", "secret", + "ipaddress", ] as const; // R33 — substring match on URL query-param keys (case-insensitive) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d579764..49f2a45 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -160,6 +160,9 @@ importers: '@repo/navigation': specifier: workspace:* 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': specifier: ^5.66.0 version: 5.96.2(react@19.2.4) @@ -567,6 +570,9 @@ importers: '@repo/core-typescript': specifier: workspace:* 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': specifier: ^19.0.0 version: 19.2.14 @@ -9435,6 +9441,31 @@ snapshots: '@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)': dependencies: '@opentelemetry/api': 1.9.1