Files
pn-new-crm/next.config.ts
Matt 92975e6bf5 feat(deps): @sentry/nextjs error tracking (DSN-gated, dormant by default)
Wires the Sentry SDK shipped-but-dormant: no-op unless
`NEXT_PUBLIC_SENTRY_DSN` is set in the environment. Production opts
in via the deploy env; dev + CI stay quiet.

- `sentry.client.config.ts` / `sentry.server.config.ts` /
  `sentry.edge.config.ts` — runtime init, each guards on the DSN.
- `instrumentation.ts` — Next 13.4+ instrumentation hook that lazy-
  imports the server + edge configs when the DSN is present.
- `next.config.ts` — withSentryConfig only wraps the config when
  the DSN is set, so dev builds skip source-map upload + middleware
  injection.
- `src/lib/env.ts` — added optional NEXT_PUBLIC_SENTRY_DSN +
  SENTRY_ENVIRONMENT + SENTRY_TRACES_SAMPLE_RATE (defaults to 0.1).

Env vars to add to .env.example (blocked from this commit by the
.env hook — apply manually):

    # Sentry (optional — SDK is a no-op without a DSN)
    NEXT_PUBLIC_SENTRY_DSN=
    SENTRY_ENVIRONMENT=
    # Defaults to 0.1 (10%) when unset
    SENTRY_TRACES_SAMPLE_RATE=

Replay is opt-in only — disabled by default for now; we'd need to
audit privacy implications (PII redaction, GDPR) before enabling it.

Verified: tsc clean, vitest 1315/1315, next build green with DSN
unset (Sentry plumbing intact, runtime no-op).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 22:38:18 +02:00

139 lines
5.1 KiB
TypeScript

import type { NextConfig } from 'next';
import bundleAnalyzer from '@next/bundle-analyzer';
import { withSentryConfig } from '@sentry/nextjs';
const isProd = process.env.NODE_ENV === 'production';
// Wrap the config with the bundle analyzer. Run `ANALYZE=true pnpm build`
// to get treemaps of the client + server bundles after the build
// completes. Pairs with the recharts dynamic-import work the audit
// flagged — gives us the tool to verify chart bundles only ship on the
// dashboard surface and not on routes that don't render them.
const withBundleAnalyzer = bundleAnalyzer({
enabled: process.env.ANALYZE === 'true',
});
/**
* Security headers applied to every response. Per audit-pass-#3 finding:
* the previous config emitted no CSP, X-Frame-Options, HSTS, or
* X-Content-Type-Options — the app was open to clickjacking + MIME
* sniffing.
*
* CSP notes:
* - 'unsafe-inline' on style-src is required by Tailwind's runtime
* style injection and Radix; revisit when Tailwind v4 ships a
* nonce story.
* - 'unsafe-eval' on script-src is dev-only — Next dev uses eval for
* HMR. Production drops it.
* - connect-src allows ws/wss for Socket.IO and https: for outgoing
* fetches; tighten in prod via per-port branding URLs once we move
* the s3 image references into a known allowlist.
* - img-src https: is wide because port branding pulls from
* s3.portnimara.com plus per-port image URLs configured at runtime.
*/
// Dev-only allow-list: react-grab (the in-page click-to-source devtool)
// is fetched from unpkg, so script/style/connect must allow it. Strip
// these entries in prod via the conditional below.
const devScriptHosts = isProd ? '' : ' http://unpkg.com https://unpkg.com';
const devConnectHosts = isProd ? '' : ' http://unpkg.com https://unpkg.com';
const csp = [
"default-src 'self'",
`script-src 'self' 'unsafe-inline'${isProd ? '' : " 'unsafe-eval'"}${devScriptHosts}`,
"style-src 'self' 'unsafe-inline'",
"img-src 'self' data: blob: https:",
"font-src 'self' data:",
`connect-src 'self' ws: wss: https:${devConnectHosts}`,
"frame-ancestors 'none'",
"base-uri 'self'",
"form-action 'self'",
"object-src 'none'",
].join('; ');
const securityHeaders = [
{ key: 'Content-Security-Policy', value: csp },
{ key: 'X-Frame-Options', value: 'DENY' },
{ key: 'X-Content-Type-Options', value: 'nosniff' },
{ key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' },
{ key: 'Permissions-Policy', value: 'camera=(self), microphone=(), geolocation=()' },
...(isProd
? [{ key: 'Strict-Transport-Security', value: 'max-age=31536000; includeSubDomains' }]
: []),
];
const nextConfig: NextConfig = {
output: 'standalone',
// Hide the floating dev indicator (the little circle/N badge in the
// corner). Compile errors still surface via the full overlay; this
// only removes the idle "everything is fine" indicator that's been
// visible in every screenshot from the iPhone testing pass.
devIndicators: false,
// LAN access from a real iPhone hits the dev server via the Mac's
// local IP (e.g. 192.168.x.x), not localhost. Next 15 surfaces a
// warning for cross-origin /_next/* fetches unless we allow-list the
// origins explicitly. Wildcard the 192.168/0.0.0.0 ranges in dev so
// any LAN device works without a config edit per network.
...(isProd ? {} : { allowedDevOrigins: ['192.168.1.42'] }),
serverExternalPackages: [
'pino',
'pino-pretty',
'bullmq',
'ioredis',
'minio',
'postgres',
'better-auth',
'nodemailer',
],
images: {
remotePatterns: [{ protocol: 'https', hostname: '*.portnimara.com' }],
},
typedRoutes: true,
outputFileTracingIncludes: {
// Bundle the EOI source PDF so the in-app EOI pathway can read it at
// runtime in the standalone build. Reading via fs.readFile from
// process.cwd() requires the file to be traced explicitly.
'/api/v1/document-templates/**': ['./assets/eoi-template.pdf'],
},
async redirects() {
return [
{
source: '/:portSlug/documents/files',
destination: '/:portSlug/documents',
permanent: true,
},
{
source: '/:portSlug/documents/files/:path*',
destination: '/:portSlug/documents',
permanent: true,
},
];
},
async headers() {
return [
{
source: '/:path*',
headers: securityHeaders,
},
];
},
};
// Sentry wrapper is conditional: if NEXT_PUBLIC_SENTRY_DSN isn't set we
// skip its build-time source-map upload + middleware injection so dev
// builds stay fast and CI doesn't need credentials. When the DSN is
// present, withSentryConfig adds instrumentation hooks that route
// errors + traces to Sentry.
const withSentry = process.env.NEXT_PUBLIC_SENTRY_DSN
? (cfg: NextConfig) =>
withSentryConfig(cfg, {
silent: true,
widenClientFileUpload: true,
// We host on our own infra — disable Vercel-specific tunneling.
tunnelRoute: undefined,
// Strip Sentry debug logger from prod bundle.
disableLogger: true,
})
: (cfg: NextConfig) => cfg;
export default withSentry(withBundleAnalyzer(nextConfig));