2 — The audit method
Tours the eight finding clusters across the app and its source, then writes findings/007-missing-priority.md as the model for every later finding.
This unit taught you to wire up a web app’s observability — Sentry, structured logs, consent-gated PostHog — and watch its performance — Core Web Vitals, the bundle analyzer, RSC waterfalls, N+1 queries. This project runs both against the invoicing SaaS you have grown across the course, the week before launch, on a branch where the observability wiring is broken and four performance regressions are planted.
Observability gaps lose data the moment a user hits the app, so you fix them: wire Sentry, harden the logger, build the consent gate. Performance gaps are slow, not bleeding, so you document them with measured impact rather than patch live code — except the barrel import, which you fix in place to capture the bundle analyzer before and after. That is what a launch review decides: a data-dropping leak closes now, a slow query goes to the backlog with a number attached.
The audit produces four artifacts, each on a surface you drive yourself:
GET /api/test/throw route, tagged with the release matching the current commit and carrying a source-mapped stack trace.$pageview that fired only after the user clicked “Accept” on the consent banner.lucide-react barrel fix, the oversized icon tile shrinking once optimizePackageImports lands.findings/SUMMARY.md quantifying coverage across the eight findings, with the two bonuses called out.EXPLAIN ANALYZE, then writing each up with the rule-location-consequence-fix template.A config inventory, each piece built out in a later lesson.
instrumentation-client.ts plus sentry.server.config.ts and sentry.edge.config.ts, booted by instrumentation.ts, wrapped by withSentryConfig in next.config.ts. Source maps upload only with SENTRY_AUTH_TOKEN at build; release tag from VERCEL_GIT_COMMIT_SHA, else 'dev'.src/lib/logger.ts with a redact drop-list and a requestId mixin. src/proxy.ts mints an x-request-id (uuidv7()) and opens a runWithContext scope over the AsyncLocalStorage in src/lib/request-context.ts; each downstream seam recovers the ID into its own scope, read by the logger and Sentry’s beforeSend.posthog-js inside a PostHogGate in src/app/_components/providers.tsx, capture off by default, gated by src/lib/analytics/consent.ts behind a ConsentProvider and a consent banner you build.src/app/(protected)/dashboard/page.tsx), authenticated layout (src/app/(protected)/layout.tsx), marketing hero (src/app/(marketing)/page.tsx), and invoice-with-customer read (src/db/queries/invoices-with-customer.ts), audited with DevTools Performance, the Turbopack analyzer, and EXPLAIN ANALYZE.findings/, with the rule-location-consequence-fix template, numbered files for findings 1–10, SUMMARY.md, out-of-scope.md, and screenshots/.The starter is the full invoicing app; most of the tree is code you will not touch. Annotated files carry a seeded finding or mark a seam a later lesson extends; the rest is uncommented. Your focus is the bolded cluster: findings 4, 5, 6, 7, and 8, plus the empty findings/ skeleton you fill in.
withSentryConfig; no optimizePackageImports (findings 1, 6)x-request-id mint/echo + runWithContext scope yet (finding 3)posthog-js at module scope, capture on, no consent gate (finding 4)<Image> missing eager-load prop (finding 7)<link> font, not next/font (bonus 9)lucide-react barrel import (finding 6)redact + no requestId mixin (findings 2, 3)SUMMARY.md, out-of-scope.md, screenshots/Files you create later are not in the tree yet. The Sentry config files (instrumentation.ts, instrumentation-client.ts, sentry.server.config.ts, sentry.edge.config.ts) appear when you wire Sentry, src/lib/request-context.ts lands with the logger seam, and the consent layer (src/lib/analytics/consent.ts, src/app/_components/consent-provider.tsx, src/app/_components/consent-banner.tsx) is built when you gate PostHog. Their absence is findings 1, 3, and 4.
2 — The audit method
Tours the eight finding clusters across the app and its source, then writes findings/007-missing-priority.md as the model for every later finding.
3 — Wire Sentry
Installs Sentry on client, server, and edge with source maps and a release tag, so the planted throw arrives decoded in the dashboard.
4 — The production logger seam
Adds one redactor shared by Pino and Sentry’s beforeSend, plus request-correlation-ID middleware backed by AsyncLocalStorage.
5 — Gate PostHog behind consent
Turns capture off by default and routes accept and reject through one consent seam, so events fire only after opt-in.
6 — Document the performance findings
Writes the waterfall and N+1 findings, fixes the barrel import for the bundle-analyzer before/after, and assembles SUMMARY.md.
7 — Verify and self-grade
Runs the verify recipe one surface at a time, commits, then diffs the work against the solution/ key to score coverage.
This starter has more moving parts than earlier projects: a Postgres database to migrate and seed, a real sign-in to clear the dashboard guard, and two optional third-party accounts. The dummy values in .env.example pass validation with no network call, so the app boots fully without real Sentry or PostHog keys. You only need real keys at the lessons that confirm events landing in a dashboard.
Get the starter codebase from the project repository, under Chapter 095/start/.
Install dependencies.
pnpm installCopy the environment template into both files: the migrate and seed scripts read .env, next dev reads .env.local.
cp .env.example .envcp .env.example .env.local(Optional) Add real Sentry and PostHog keys to see live events. Skip this until a lesson asks for it.
Start the Postgres container.
docker compose up -dRun the migrations.
pnpm db:migrateSeed the database.
pnpm db:seedStart the dev server.
pnpm devOpen http://localhost:3000/sign-in and sign in as the seeded admin: alice@example.com / inspector-password-12 (the SEED_PASSWORD constant in scripts/seed.ts).
The optional third-party keys, for the lessons that need them:
| Variable | Purpose | How to obtain |
|---|---|---|
NEXT_PUBLIC_SENTRY_DSN | The endpoint Sentry events are sent to. | Create a free-tier Sentry org and project; the DSN is under Project Settings → Client Keys. |
SENTRY_ORG, SENTRY_PROJECT | The project the build uploads source maps to. | The org and project slugs from your Sentry account. |
SENTRY_AUTH_TOKEN | Gates source-map upload at build time. | Settings → Auth Tokens, scoped for source-map upload. |
NEXT_PUBLIC_POSTHOG_KEY, NEXT_PUBLIC_POSTHOG_HOST | The PostHog project key and ingestion host. | Create a free-tier PostHog project; both values are on its setup page. |
Expected result. The app boots at http://localhost:3000: a marketing landing page, an authenticated dashboard, and an invoice list seeded under org_acme, roughly 30 customers, 240 invoices, and three or more members. The /dashboard route needs a real Better Auth session, so without the sign-in from step 9, proxy.ts redirects you to /sign-in. Everything else starts broken: Sentry is unwired so errors vanish, the logger leaks the Stripe signature in the clear, PostHog captures on first load before any consent, and the four performance regressions are live. That is the state the rest of the chapter resolves.