Observability
Set up error monitoring and analytics, and track down why an event didn't arrive.
Litestore reports errors and performance data to Sentry, and it sends product analytics to the destinations you configure. These two systems are unrelated, and the sections below cover both.
Optional services
The app runs without either system. If you leave a provider unconfigured, you lose that data, but the store stays up.
That choice has a side effect to keep in mind from the start. Nothing described here fails loudly. A misconfigured destination looks exactly like a destination that has nothing to report.
Sentry
Next.js has three runtimes, and Litestore sets up Sentry in four files to cover them.
instrumentation.ts is the register hook. It loads the server or edge file depending on the runtime, and it exports onRequestError. instrumentation-server.ts covers the Node server, instrumentation-edge.ts covers the Edge runtime, and instrumentation-client.ts covers the browser.
Each of the three runtime files starts Sentry only when NEXT_PUBLIC_SENTRY_DSN is set. Without a DSN, no runtime creates a Sentry client.
Sentry activation
The server and edge files start Sentry whenever NODE_ENV isn't development, and the browser file only in production. Errors from bun run dev never show up in the issue list.
The build wrapper follows the same rule. Litestore applies withSentryConfig only on a production Vercel build, and source maps upload only in CI.
The browser file has one exception. If you set NEXT_PUBLIC_SENTRY_DEV to true, the browser SDK starts outside production too. Use it when you need to prove that browser reporting works.
Sampling
All three runtimes send 10% of traces.
The browser also records session replays. It masks all text and blocks all media. It records 10% of sessions, and every session that has an error.
Each runtime drops errors that count as noise before sending anything. All runtimes drop chunk-loading failures and the Failed to fetch and NetworkError type errors. The browser also drops ResizeObserver loops, errors from browser extensions and ZodError form validation errors, since those come from normal user input and not from a bug.
In production, Sentry drops console breadcrumbs.
Because of these filters, a quiet Sentry doesn't prove the app is error-free. To test the integration, throw an error that isn't on these lists.
Server-side forwarding
modules/analytics/server-forward.ts sends events to three destinations. Each destination checks only its own keys, so one works without the others.
| Destination | How Litestore sends to it | Keys it needs |
|---|---|---|
| PostHog | Its capture API | POSTHOG_API_KEY |
| GA4 | The Measurement Protocol | GA4_MEASUREMENT_ID and GA4_API_SECRET |
| Meta | The Conversions API | META_PIXEL_ID and META_ACCESS_TOKEN |
Litestore sends to all three with Promise.allSettled. If one destination is slow or fails, the request doesn't wait for it.
Two places call this code. The /api/track route handles beacons from the browser and adds details from the request. The payment webhook sends purchase events and adds details from the order metadata.
The webhook used to POST to /api/track over HTTP. That approach had three problems. It started a second serverless function for every purchase. It ran the shopper rate limiter against the function's own IP. And it couldn't add any details, because an internal request has no shopper cookies.
Meta events include an eventId. When the browser Pixel and the Conversions API send the same id, Meta counts them as one event instead of two.
Plausible only runs in the browser
Plausible is not one of the server destinations. It runs as a browser tracker, so it never receives an event that doesn't happen in a browser, such as an order created by a webhook.
Browser trackers
modules/analytics/consent-gated-trackers.tsx loads nothing until the shopper gives consent.
The component waits until it has read the shopper's consent and found it granted. During server rendering and the first paint it renders nothing, because consent can only be read in the browser.
| Consent group | Trackers it loads |
|---|---|
| Analytics | Plausible and GA4 |
| Marketing | The Meta Pixel and the TikTok Pixel |
Each tracker also needs its own public environment variable. If you never configured a tracker, granting its group doesn't load it.
The component checks again when the consent-changed event fires and when a storage event arrives from another tab.
There is one gap, and it comes from how scripts work, not from a bug. Removing a script from the page can't undo a pixel that has already loaded. Since nothing loads before consent, this only matters when a shopper withdraws consent they already gave in the same session. The next page load respects the change.
Production check
Deploying the code doesn't turn data collection on. You still need to enable each destination and prove it collects data in the environment you intend.
Test log line
Then find it in the deployment logs.
Test error
Confirm that its stack trace resolves to your source.
Page views
Confirm the page views arrive.
Use separate Sentry environments or projects for preview and production. That way a test error never pages someone about production.
Missing events
Start with the narrowest check and work outward.
Most of these cases come down to one question:
Which runtime should this event have started in, and does that runtime report anywhere?
Related
Configuration
env.ts requires some environment variables, and each optional group configures one service.
Deploying
A configured store goes to production with bun run db:deploy, the Next.js build and the Inngest functions served from /api/inngest.
Events
Litestore publishes its domain events through Inngest, handlers stay idempotent, and delivery is best-effort.