Observability
Learn where Sentry is instrumented, which three destinations receive server-side events, and why Plausible is browser-only.
Observability splits into two unrelated concerns: error and performance monitoring through Sentry, and product analytics forwarded to whichever destinations you configure.
Neither is required for the app to run. An unconfigured provider costs you telemetry rather than availability.
That is deliberate, and it has a consequence worth naming up front: nothing here fails loudly. A destination that is misconfigured looks exactly like a destination that is quiet.
Sentry
Four instrumentation files cover the runtimes Next.js exposes:
instrumentation.ts— the register hook, which dispatches to the server or edge file by runtime, and exportsonRequestErrorinstrumentation-server.ts— Node serverinstrumentation-edge.ts— Edgeinstrumentation-client.ts— Browser
Each of the three runtime files initialises Sentry only when NEXT_PUBLIC_SENTRY_DSN is set. No DSN means no client, in every runtime.
What is gated
Sentry initialisation is gated to CI and production Vercel deployments, so local development does not report into your production issue stream.
The build wrapper is gated the same way. withSentryConfig is only applied on a production Vercel build, and source maps upload only in CI.
The browser file has one escape hatch: setting NEXT_PUBLIC_SENTRY_DEV to true initialises the browser SDK outside production, for when you need to prove the client path works.
Sampling and noise
Traces are sampled at 10% in all three runtimes.
The browser adds session replay, masking all text and blocking all media, at 10% of sessions and 100% of sessions with an error.
Each runtime filters its own known noise before sending — chunk-loading failures, Failed to fetch and NetworkError type errors, and in the browser also ResizeObserver loops, browser-extension errors, and ZodError form validation, which is expected user input rather than an application fault.
Console breadcrumbs are dropped in production.
The filters are why a quiet Sentry is not proof of a quiet application. If you are testing the integration, throw something that is not on those lists.
Server-side forwarding
lib/analytics/server-forward.ts posts to exactly three destinations:
- PostHog — capture API, gated on
POSTHOG_API_KEY - GA4 — Measurement Protocol, gated on
GA4_MEASUREMENT_IDandGA4_API_SECRET - Meta — Conversions API, gated on
META_PIXEL_IDandMETA_ACCESS_TOKEN
Each destination is gated on its own environment keys, so configuring one does not require configuring the others.
All three are dispatched with Promise.allSettled, so one slow or failing destination cannot hold up a request.
This is one implementation with two callers: the /api/track route, which handles browser beacons and enriches them from the request, and the payment webhook, which sends purchase events enriched from order metadata.
The webhook used to POST to /api/track over HTTP. That was a second serverless invocation per purchase, it ran the shopper rate limiter against the function's own IP, and it could enrich nothing — an internal request carries no shopper cookies.
Meta events carry an eventId. Sending the same id to the browser Pixel and the Conversions API collapses the two into one event instead of double-counting it.
Plausible is browser-only
Plausible is not in the server-forward path. It runs as a browser tracker, so an event that never reaches a browser — a webhook-driven order, for example — does not appear in it.
Browser trackers
components/web/consent-gated-trackers.tsx is default-deny.
Nothing loads until consent is read and granted. The component renders nothing during SSR and first paint, because consent is only readable on the client.
The grouping is deliberate:
- Analytics consent loads Plausible and GA4
- Marketing consent loads the Meta Pixel and the TikTok Pixel
Each tracker also needs its own public environment variable set, so granting a category does not load a tracker you never configured.
It re-evaluates on the consent-changed event and on cross-tab storage events.
One limitation is structural rather than a bug: unmounting a script cannot retract a pixel that has already loaded. Because the default is deny, nothing loads before consent, so the only gap is withdrawing an already-granted category within the same session. The next page load respects it.
Verifying it in production
Deploying the code is only the first step. Each destination has to be enabled and proven in the environment you expect it to collect from.
- Write a uniquely named log line and find it in the deployment logs
- Send one controlled test error and confirm its stack trace resolves
- Visit two or three routes and confirm the page views arrive
- Remove the test error
Use separate Sentry environments or projects for preview and production, so a deliberate test failure never pages someone about production.
When an event is missing
Work from the narrowest gate outward.
If nothing at all arrives, check the environment keys first. Every destination here is individually gated, and an unset key is silence, not an error.
If browser events are missing but server events arrive, the gate is consent. Analytics and marketing are separate categories, and a tracker whose public variable is unset never renders regardless of consent.
If server events arrive but one destination is empty, the gate is that destination's own key pair. Promise.allSettled means the other two succeeded without telling you the third did not.
If the event never touched a browser at all — a webhook-driven order, a cron, an admin action — then Plausible was never going to see it, and no amount of consent configuration will change that.
The question that resolves most of these:
Which runtime was this event supposed to originate in, and is that runtime instrumented at all?
Related
Configuration
Learn which environment variables env.ts requires, which ones are optional, and what each optional group configures.
Deploying
Learn how to take a configured store to production: bun run db:deploy for migrations, the Next.js build, and the Inngest functions served from /api/inngest.
Events
Learn how the 56 domain events publish through Inngest, how handlers stay idempotent, what the crons do, and why delivery is best-effort rather than guaranteed.
Providers
Learn which external services sit behind a swappable interface, which are integrated directly, and which environment variables the app refuses to boot without.
Overview
Learn how to run the unit and integration lanes, what the invariant and contract tests assert, and which harness check keeps money and inventory covered.