Observability
This page helps you set up error monitoring and analytics and find out 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 this page covers both.
The app runs without either of them. If you leave a provider unconfigured, you lose that data, but the store stays up.
That choice has a side effect you should know about from the start. Nothing on this page 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.tsis the register hook. It loads the server or edge file depending on the runtime, and it exportsonRequestError.instrumentation-server.tscovers the Node server.instrumentation-edge.tscovers the Edge runtime.instrumentation-client.tscovers 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.
When Sentry is on
Sentry only starts in CI and on production Vercel deployments. Your local development errors never show up in your production 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 and noise
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 are known 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
lib/analytics/server-forward.ts sends events to three destinations:
- PostHog through its capture API, when
POSTHOG_API_KEYis set - GA4 through the Measurement Protocol, when
GA4_MEASUREMENT_IDandGA4_API_SECRETare set - Meta through the Conversions API, when
META_PIXEL_IDandMETA_ACCESS_TOKENare set
Each destination checks only its own keys, so you can set up one without the others.
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 sees an event that doesn't happen in a browser, such as an order created by a webhook.
Browser trackers
components/web/consent-gated-trackers.tsx loads nothing until the shopper agrees.
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 comes in two groups:
- 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. 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.
Checking it in production
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 expect.
- Write a log line with a unique name. Then find it in the deployment logs.
- Send one test error on purpose. Confirm that its stack trace resolves to your source.
- Visit two or three pages. Confirm the page views arrive.
- Remove the test error.
Use separate Sentry environments or projects for preview and production. That way a test error never pages someone about production.
When an event is missing
Start with the narrowest check and work outward.
If nothing arrives at all, check the environment keys first. Every destination has its own keys, and a missing key produces silence, not an error.
If server events arrive but browser events don't, the cause is consent. Analytics and marketing are separate groups. A tracker whose public variable isn't set never loads, whatever the consent says.
If server events reach two destinations but not the third, check that destination's own keys. Promise.allSettled lets the other two succeed without telling you the third one failed.
If the event never happened in a browser, Plausible was never going to see it. That covers webhook orders, crons and admin actions, and changing the consent setup won't help.
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
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.