Skip to content
Chapter 95Lesson 1

Project overview

You have spent this unit learning the two halves of running a SaaS in the dark and turning the lights on: the wiring half — Sentry catching errors, structured logs you can read at 3am, PostHog gated behind consent — and the vigilance half — Core Web Vitals, the bundle analyzer, RSC waterfalls, N+1 queries. This project closes the unit by running both halves against one seeded app the week before it launches. The audit target is the same invoicing SaaS you have been growing across the course, but this branch ships with its observability wiring deliberately missing or broken and four performance regressions planted in the code. Your job is a hybrid wire-and-document audit: the observability gaps lose data the moment a user hits the app, so you fix them — you wire Sentry, harden the logger, and build the consent gate. The performance gaps are slow, not bleeding, so you document them — you write a findings report with measured impact rather than patching live code, with one exception: you fix the barrel import in place so you can capture the bundle-analyzer before and after as evidence. That split is the whole point. An experienced engineer at a launch review does not treat every problem the same way; a leak that drops error data closes before launch, a slow query goes to the backlog with a number attached.

The audit target — the invoicing SaaS you grow across the course, here booted locally and signed in as the seeded admin. This running app is your primary diagnostic surface: you hold it open beside the source and read each finding's fingerprint off it before you write or wire anything.

By the end, the finished audit produces four artifacts — the four things an experienced engineer confirms at a launch review. None appears above: each lives on a different surface you drive yourself — Sentry’s dashboard, PostHog’s dashboard, the Turbopack analyzer, and a Markdown file — so you will not see them side by side until you have run each tool against the app above.

  • A Sentry event captured from the deliberate GET /api/test/throw route — tagged with the release matching the current commit, carrying a readable source-mapped stack trace.
  • A PostHog $pageview that fired only after the user clicked “Accept” on the consent banner, and nothing before it.
  • The Turbopack bundle-analyzer treemap, before and after the lucide-react barrel fix — the oversized icon tile shrinking sharply once optimizePackageImports is in place.
  • A filled findings/SUMMARY.md quantifying coverage across the eight findings, with the two bonuses called out.
  • Auditing a running app for observability gaps and performance regressions — treating the running state as the diagnostic surface, not source-reading alone.
  • Wiring Sentry across the client, server, and edge runtimes with source maps and release tags.
  • Building a single redactor seam and a request-correlation-ID middleware reused by both the logger and Sentry.
  • Consent-gating analytics so capture starts only on an explicit opt-in.
  • Diagnosing performance issues from traces, the bundle analyzer, and EXPLAIN ANALYZE, then writing them up with the rule-location-consequence-fix template.
  • Self-grading a deliverable against a reference answer key — the audit’s senior-reach habit.

The audit installs four clusters of wiring and audits four performance surfaces, then collects everything into one deliverable. This is a config inventory, not a request flow — the shape of what gets wired and what gets audited, nothing about how yet. Each piece is the work of a later lesson.

  • Sentryinstrumentation-client.ts (client init) plus sentry.server.config.ts and sentry.edge.config.ts, booted by instrumentation.ts and wrapped into the build by withSentryConfig in next.config.ts. Source maps upload only when SENTRY_AUTH_TOKEN is present at build time; the release tag is computed from VERCEL_GIT_COMMIT_SHA with a 'dev' fallback.
  • Structured logging — Pino in src/lib/logger.ts carrying a redact drop-list and a requestId mixin. src/proxy.ts mints an x-request-id (uuidv7()) and opens a runWithContext scope over an AsyncLocalStorage defined in src/lib/request-context.ts; each downstream seam — the Stripe webhook handler, for instance — recovers that ID from the header and opens its own scope, which both the logger and Sentry’s beforeSend read.
  • Analyticsposthog-js loaded inside a PostHogGate in src/app/_components/providers.tsx, with capture off by default, gated by src/lib/analytics/consent.ts behind a ConsentProvider and a consent banner you build from scratch.
  • Performance surfaces — the RSC dashboard (src/app/(protected)/dashboard/page.tsx), the authenticated layout (src/app/(protected)/layout.tsx), the marketing hero (src/app/(marketing)/page.tsx), and the dashboard’s invoice-with-customer read (src/db/queries/invoices-with-customer.ts) — audited through DevTools Performance, the Turbopack analyzer, and EXPLAIN ANALYZE.
  • Deliverablefindings/, holding 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, so most of the tree is the codebase you already know and will not touch. The annotated files below each carry a seeded finding or are a seam a later lesson extends; everything else is uncommented. The bolded cluster is your focus — the files carrying findings 4, 5, 6, 7, and 8, plus the empty findings/ skeleton you will fill in.

  • next.config.ts not wrapped with withSentryConfig; no optimizePackageImports (findings 1, 6)
  • .env.example Sentry + PostHog key names and how to obtain them
  • Directorysrc/
    • env.ts to extend: Sentry env keys land here (finding 1)
    • proxy.ts no x-request-id mint/echo + runWithContext scope yet (finding 3)
    • Directoryapp/
      • Directory_components/
        • providers.tsx imports posthog-js at module scope, capture on, no consent gate (finding 4)
      • Directory(marketing)/
        • page.tsx hero <Image> missing eager-load prop (finding 7)
        • layout.tsx raw <link> font, not next/font (bonus 9)
      • Directory(protected)/
        • layout.tsx lucide-react barrel import (finding 6)
        • Directorydashboard/
          • page.tsx sequential awaits, RSC waterfall (finding 5)
      • Directoryapi/test/throw/
        • route.ts the deliberate-throw proof target (finding 1)
    • Directorylib/
      • logger.ts Pino with no redact + no requestId mixin (findings 2, 3)
    • Directorydb/queries/
      • invoices.ts healthy: uses the relations API (must stay healthy)
      • invoices-with-customer.ts 1 + N customer lookups (finding 8)
  • Directoryfindings/ ships empty: template, 001–010 placeholders, SUMMARY.md, out-of-scope.md, screenshots/

Notice what is not in the tree yet — the files you create as you wire each fix. 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 from nothing when you gate PostHog. The starter ships none of them — that absence is finding 1, finding 3, and finding 4.

2 — The audit method

Tours the eight finding clusters across the running app and its source, then writes findings/007-missing-priority.md end to end as the chapter’s reference shape.

3 — Wire Sentry

Installs Sentry across client, server, and edge with source maps and a release tag, so the deliberate throw lands decoded in the dashboard.

4 — The production logger seam

Adds the single redactor reused by Pino and Sentry’s beforeSend, plus a request-correlation-ID middleware backed by AsyncLocalStorage.

5 — Gate PostHog behind consent

Flips 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 in place for the bundle-analyzer before/after, and assembles SUMMARY.md.

7 — Verify and self-grade

Runs the full verify recipe one surface at a time, commits, then diffs the work against the solution/ answer 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 get past the dashboard guard, and a pair of optional third-party accounts if you want to watch live events flow. The third-party keys are genuinely optional: the dummy values in .env.example pass validation with no network round-trip, so the app boots fully without them. You only need real keys when you reach the lessons that confirm events landing in a dashboard.

  1. Get the starter codebase from the project repository, under Chapter 095/start/.

  2. Install dependencies.

    Terminal window
    pnpm install
  3. Copy the environment template. The migrate and seed scripts read .env; next dev reads .env.local, so create both.

    Terminal window
    cp .env.example .env
    cp .env.example .env.local
  4. (Optional) Populate the real Sentry and PostHog keys if you want to see live events. The dummy values already in the file pass validation, so skip this for now and come back when a lesson asks for it.

  5. Start the Postgres container.

    Terminal window
    docker compose up -d
  6. Run the migrations.

    Terminal window
    pnpm db:migrate
  7. Seed the database.

    Terminal window
    pnpm db:seed
  8. Start the dev server.

    Terminal window
    pnpm dev
  9. Open 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, when you reach the lessons that need them:

| Variable | Purpose | How to obtain | | --- | --- | --- | | NEXT_PUBLIC_SENTRY_DSN | The client/server 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 | Identify 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 to allow 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 the project’s setup page. |

Expected result. The app boots on http://localhost:3000 with 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 requires a real Better Auth session; without one, proxy.ts redirects you to /sign-in, which is why step 9 signs you in. At this point everything is running in its broken starting state: 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 all live. That is exactly the state the rest of the chapter resolves.