As engineers, we deal with bugs every day. Finding the root cause and delivering the right fix is one of our most essential — and satisfying — skills.
In this post, I want to walk through different types of issues we face and how we typically debug them. Most importantly, I’ll share my favorite and most challenging method: data-driven debugging.
Let’s break it down by who reports the issue and how it’s discovered.
1. Developer-Reported Issues
When you’re writing code, you’re usually writing tests too. You may not be doing full-blown Test-Driven Development (TDD), but adding tests is still the cheapest and easiest way to ensure correctness.
Unit tests are fast and stable — great for checking individual functions or modules. However, they only verify isolated parts of the system. So even when all unit tests pass, your app may still break when everything is put together.
That’s where end-to-end (E2E) tests come in. They’re harder to write, more brittle, but more valuable for verifying real-world behavior.
A typical debugging flow here is: a test fails → you inspect the code → fix the bug → all tests pass. Clean and efficient.
This is the easiest phase for catching and fixing bugs — the feedback is immediate, and you’re already deep in the context of the code.
2. QA-Reported Issues
Next, we have bugs found by QA — the professional bug hunters. Having a second set of eyes on the feature often reveals blind spots, especially around misunderstood requirements or unexpected user flows.
When QA finds a bug, they’ll usually provide a ticket with preconditions, steps to reproduce, and a comparison of expected vs. actual results.
Your first job? Reproduce the issue. From there, you might inspect logs, step through the debugger, or trace the flow in the code. Once you’ve isolated the problem, write a test and push a fix.
This stage is still relatively efficient — you get a clear report with reproduction steps, which makes isolating the issue much easier.
3. User-Reported Issues
This is when things get trickier. Now the feature is live, and real users are discovering problems.
Sometimes they report it via in-app feedback tools. Other times, it’s buried in a one-star review. Either way, the report is usually vague. Users don’t speak in stack traces or HTTP error codes — they describe symptoms, not causes.
As a developer, you’ll need to interpret what they meant, recreate the issue, and try different approaches to break the app the same way.
Some bugs are easy to reproduce. Others might only show up on certain devices, or under rare timing conditions.
User reports are harder to act on, but they often highlight real-world edge cases you never thought about during development.
4. Metrics-Driven Issues
Now we enter the most complex — and, honestly, the most fascinating — form of debugging.
Here, no one reports a bug. Instead, you’re monitoring system metrics: page load times, crash rates, memory usage, API latency, conversion funnels — anything critical to the health of your product.
One day, a metric goes off. Maybe there’s a spike. Or maybe a slow, steady trend downward. It doesn’t look right, but there’s no specific error. No user complaint. Just a number telling you: something’s wrong.
Sometimes, this correlates with user-reported issues. Great — now you have clues. But often it doesn’t. For example, users don’t notice a 20% increase in network traffic. But your servers and infrastructure budget definitely will.
So how do you debug without a clear bug?
This is where data-driven debugging comes in. You form hypotheses, slice the data into segments — by country, app version, device type, user behavior — and hunt for patterns.
If a metric looks fine across all Android versions but is broken only on iOS 17.3, you’ve got a lead. If new users are fine but returning users aren’t, that’s another breadcrumb.
You explore the data, test theories, and either validate or reject them. Each rejected hypothesis is still a valuable learning — it narrows the search space and builds context.
This is the most difficult and time-consuming form of debugging, but also the most rewarding. When you finally crack the mystery and fix the root cause — it feels amazing.
What Is Data-Driven Debugging?
It’s a systematic process of using your system’s data — metrics, logs, analytics — to discover issues that aren’t easily surfaced by tests or user reports.
How It Works
- Start by identifying the metric that’s off.
- Slice your data across dimensions: versions, countries, segments, device types, etc.
- Look for patterns and anomalies.
- Form and test hypotheses.
- Dig deeper where you find differences. Move on where you don’t.
- Supplement this with manual testing, code inspection, or rollback experiments if needed.
It’s time-intensive. Writing and running queries isn’t cheap. You need to prioritize where to dig first. And sometimes, analytics are missing, or have changed recently, which adds even more complexity.
But as you get more comfortable with your tools and systems, you build an intuition for where to look and how to spot weak signals.
Data-driven debugging feels impossible at first, but over time you get faster, smarter, and better at uncovering hidden issues in production systems.
Final Thoughts
In modern engineering, working with data isn’t optional — it’s a superpower. Data-driven debugging gives you the ability to find and fix issues before they become catastrophic.
Want to get started? Set up some core metrics for your product today. Monitor them. One day, something will go off — and when it does, you’ll be ready to dig in.
Happy coding