Frontend Observability Without Drowning Your App in Tracking Scripts
For a long time my idea of frontend observability was "install Sentry, wire up the DSN, ship it." That gets you real value on day one — genuinely, do this first if you've done nothing else — but it also gave me a false sense that I was "doing observability" when really I had error reporting and nothing else. It took a production incident where our checkout page was silently taking eleven seconds to become interactive for a meaningful chunk of mobile users, with zero errors thrown anywhere, for me to realize error tracking and observability are not the same thing.
Errors are the easy 20%
Sentry, or Bugsnag, or Rollbar — pick one, they're all fine — will catch your unhandled exceptions, your promise rejections, your React error boundaries tripping. Set it up in an afternoon:
import * as Sentry from '@sentry/react';
Sentry.init({
dsn: process.env.SENTRY_DSN,
integrations: [Sentry.browserTracingIntegration()],
tracesSampleRate: 0.1,
environment: process.env.NODE_ENV,
beforeSend(event) {
if (event.request?.headers) delete event.request.headers['Authorization'];
return event;
},
});
That tracesSampleRate: 0.1 line matters more than people give it credit for. At 100% sampling on a moderately trafficked app you will bury your Sentry quota and your own attention in noise within a week. Ten percent is a reasonable starting point; you can always bump it temporarily when you're actively debugging something.
But errors only tell you about the things that threw. The eleven-second checkout page didn't throw anything — it just quietly failed to be fast, and nobody was watching for that because we hadn't instrumented it. This is the part of observability that's genuinely hard: deciding what to measure when nothing is technically broken.
What's actually worth instrumenting
I've settled on three categories, and I'd encourage resisting the urge to go beyond them until each one is actually being looked at by a human on some cadence.
Core Web Vitals, sampled, sent somewhere you'll see them. Largest Contentful Paint, Interaction to Next Paint, Cumulative Layout Shift. The web-vitals library makes this almost embarrassingly easy to wire up:
import { onLCP, onINP, onCLS } from 'web-vitals';
function sendToAnalytics(metric) {
if (Math.random() > 0.2) return; // sample 20% of sessions
navigator.sendBeacon('/api/vitals', JSON.stringify({
name: metric.name,
value: metric.value,
id: metric.id,
path: window.location.pathname,
}));
}
onLCP(sendToAnalytics);
onINP(sendToAnalytics);
onCLS(sendToAnalytics);
The sendBeacon call matters — it fires and forgets without blocking navigation, which is exactly what you want for a metric ping that shouldn't cost the user anything.
Business-critical user flows, instrumented as spans, not just page loads. For an e-commerce site, that's add-to-cart through order confirmation. For a SaaS product, it's usually onboarding through first meaningful action. I wrap these with performance.mark and performance.measure rather than reaching straight for a vendor SDK, because the browser Performance API is vendor-neutral and I can pipe it wherever I want later:
performance.mark('checkout-start');
// ... user goes through checkout ...
performance.mark('checkout-complete');
performance.measure('checkout-duration', 'checkout-start', 'checkout-complete');
const [measure] = performance.getEntriesByName('checkout-duration');
reportMetric('checkout_duration_ms', measure.duration);
Console and network errors that don't throw but still matter — failed API calls that get silently caught, 4xx and 5xx responses, GraphQL responses that return errors in the body with a 200 status code (a genuinely evil pattern that will hide real failures from every error tracker you install unless you specifically check for it).
Notice what's not on this list: click tracking on every button, mouse movement heatmaps, scroll depth on marketing pages, session replay running on 100% of traffic. Those aren't useless, but they're expensive — in bundle size, in privacy surface area, in the sheer volume of data nobody ends up querying — and they're usually a product analytics concern (Amplitude, PostHog, Mixpanel) rather than an engineering observability concern. Conflating the two is how you end up with a page that ships six different tracking scripts and a Lighthouse score that makes you wince.
Sampling is not optional past a certain scale
Early on, at low traffic, you can log everything and it costs you nothing. Once you're past a few thousand daily active users, unsampled tracing will bankrupt your observability budget and, more importantly, make your dashboards useless because the signal drowns in volume. My rule of thumb: sample error tracking around 10-25% depending on volume (errors matter enough that you want a strong signal, but you rarely need every single instance), sample performance traces around 5-10% (performance distributions are stable enough that a sample gives you an accurate p50/p75/p95 without full capture), and sample session replay, if you use it at all, in the low single digits and only trigger it on error or rage-click events rather than recording everyone by default.
Sentry.init({
dsn: process.env.SENTRY_DSN,
replaysSessionSampleRate: 0.01,
replaysOnErrorSampleRate: 1.0, // always capture replay when there's an actual error
});
That last line is the trick that made session replay actually worth the bundle-size cost for us — record almost nobody by default, but always record the sessions where something went wrong. You get the debugging value without the storage bill or the privacy exposure of recording every visitor's mouse movements.
Avoiding observability theater
The phrase I keep coming back to is "observability theater" — dashboards that exist, get built with real effort, and then nobody opens them again after week two. I've built a few of these myself and I can now spot the warning signs. If a metric doesn't have a named owner who gets paged or at least pinged when it crosses a threshold, it's decoration. If a dashboard requires more than about ten seconds of interpretation to answer "is this good or bad right now," it's decoration. If you added an integration because a vendor demo looked impressive rather than because you had a specific question you couldn't answer without it, it's very likely decoration.
The fix that worked for our team was blunt: every metric we instrument gets one line in a shared doc — what it measures, what threshold triggers concern, who owns it. If we can't fill in the "who owns it" column, we don't ship the instrumentation. That constraint alone killed off about a third of what I would have otherwise added, and the dashboards that survived are ones people genuinely check during incidents.
One last thing worth saying: instrumentation is a tax on every page load, so treat your observability bundle size as a real budget line, not an afterthought. Check what your tracking libraries actually cost with something like webpack-bundle-analyzer or the Network tab in Chrome DevTools before you accept the default install instructions from any vendor. Sentry's browser SDK with tracing and replay disabled is a few kilobytes gzipped; turn on every feature and it's meaningfully more. Measure it, decide if it's worth it for your app, and revisit that decision every few months as your needs and traffic change — what was overkill at 10,000 monthly users might be exactly right at 500,000, and the reverse is just as often true.
Closing the loop with alerting, not just dashboards
The last piece that took me too long to get right was realizing a dashboard by itself does nothing at 2am when nobody's looking at it. Every metric that matters enough to instrument deserves a threshold-based alert wired into whatever your team already checks — Slack, PagerDuty, an on-call rotation — rather than living purely as a line on a chart someone might glance at during a weekly review. We set a simple rule: if INP at the 75th percentile crosses 500ms for more than fifteen minutes sustained, or if the error rate on a Sentry-tracked release jumps more than three times the trailing seven-day baseline, it pages whoever's on rotation that week, the same way a backend outage would. Treating frontend degradation with the same seriousness as backend downtime was a genuine culture shift for us, and it only became possible once the instrumentation from this post was actually in place and trustworthy enough to alert on without generating constant false positives.
Related Posts
Sponsor Our Newsletter
Reach thousands of developers who are actively evaluating AI tools, MCP servers, and dev infrastructure. Our weekly newsletter goes to engaged technical decision-makers.
All sponsored content is clearly labeled per our editorial policy.