The Agent and Collector Design
In one line: this is the design that actually solves the problem. It inverts probing — instead of you sampling the network, the users tell you — which fixes the coverage ceiling by construction.
The two components
| Component | Responsibility |
|---|---|
| Agent | A module embedded in the client application that sends reports when failures occur |
| Collector | A standalone service that receives error reports. Independent of the primary service, so it remains reachable even when the main service fails. Reports are analyzed to detect spikes in client-side errors |
The essential move is in that diagram: the client cannot reach the service, and it reports that fact somewhere else. If the report went to the primary service it would fail for exactly the same reason as the original request — which is why the collector's independence is not a nice-to-have but the entire premise.
Why this beats probing on coverage
Probing samples the network from wherever you happened to place a prober. Agents report from wherever your users actually are — every autonomous system, ISP, mobile network, and corporate proxy that has a real customer behind it.
Coverage stops being something you buy and becomes a property of having users. That inverts the coverage problem: the networks you most need visibility into are, by definition, the ones with users in them.
The two are complementary. Probing works with zero users and no consent; agent reporting works everywhere your users are and reflects real conditions.
Scaling the collector tier
Collectors are organized in a hierarchical aggregation architecture. Two supporting decisions:
| Decision | Detail |
|---|---|
| Deploy close to client networks | Reduces latency — reports arrive from a nearby collector rather than crossing the world |
| Stream processing | Enables near real-time analysis, so a spike is visible in seconds rather than after a batch job |
Sampling
To control costs, the system may sample reports — for example collecting data from 1% of users — instead of capturing every event. A system that ingests and processes every event would require substantially more infrastructure and cost.
Why 1% is enough here, when it wouldn't be elsewhere
Sampling works because of what you are looking for. The goal is detecting a spike in client-side errors, not reconstructing any individual user's session.
A regional outage affects a large fraction of users in that region, so even a 1% sample surfaces it clearly — you are estimating a rate, and rates survive sampling. Contrast that with distributed tracing, where you often need the specific trace and sampling can lose exactly the one you wanted.
Worth saying out loud in an interview: sample when you need a rate, keep everything when you need an instance.
What the collector does with ten million reports
Collecting is half the system. A large client fleet in a bad hour produces millions of error reports, and a list of millions of errors is not more useful than no errors — it is less, because nobody reads it. The processing side is where a client-error system becomes usable, and it is the part interviewers probe once the collection story is done.
Grouping: millions of reports, dozens of problems
Almost every report is a duplicate of another. One broken deploy produces the same failure on every device that hits it, so the raw stream is enormously redundant.
A fingerprint is a stable hash over the parts of a report that identify the fault rather than the occurrence — typically the error type plus the top few frames of the stack, with the volatile parts deliberately excluded.
Choosing what to exclude is the whole difficulty, and it fails in both directions:
| Fingerprint too NARROW | Fingerprint too BROAD |
|---|---|
| Includes volatile data — a user ID, a timestamp, an interpolated value in the message | Includes only the error type |
| Every occurrence is its own issue. Ten million issues of count 1 — you have re-created the raw list | Unrelated faults merge. Every TypeError in the application becomes one issue, so a new regression hides inside an old bucket |
| Nothing is ever recognised as recurring | The count is meaningless and the first-seen date is years old |
The signal worth stating: rank issues by distinct users affected, not by report count. One user in a retry loop can generate a hundred thousand reports and outrank a bug hitting ten thousand people once each. Report count measures the loudest client; user count measures the actual problem.
And tag every report with the client release version, because the first question about any new issue is which deploy introduced it. An issue whose first-seen coincides with a release is diagnosed before anyone reads the stack trace — which is also what makes the headline metric worth computing: crash-free sessions per release, rather than a raw error total that grows with traffic.
Symbolication: the stack trace you receive is unreadable
Shipped client code is minified or compiled. What arrives looks like this:
TypeError: undefined is not an object at t (main.4f9a2.js:1:48213) at r (main.4f9a2.js:1:52907)
Which names no function, no file, and no line anyone can act on — and worse, it defeats fingerprinting too, since a rebuild changes those offsets and every issue looks new after every deploy.
Two consequences worth volunteering. The source map must be uploaded at build time and keyed by the exact build identifier, because it is only valid for the artifact it was generated from — which means the pipeline needs the build ID in the report. And source maps must not be served publicly alongside the bundle: doing so hands anyone your original source. Upload them to the monitoring system and keep them out of the CDN.
Error storms
The last processing concern is protective rather than analytical.
This is the same shape as the logging chapter's incident-time volume problem: error volume spikes precisely when the pipeline is most needed. Two caps handle it — a per-client budget so one looping device cannot dominate, and per-fingerprint sampling once a group is well established, since the ten-thousandth instance of a known issue adds a count and nothing else. Keep counting all of them; stop storing all of them.
The three questions this raises
Embedding a reporter in the client creates three problems the rest of the chapter addresses:
| Question | Covered in |
|---|---|
| Can a user activate and deactivate client-side reports? | Lesson 5 |
| How do agents reach collectors under faulty conditions? | Lesson 6 |
| How do we protect user privacy? | Lesson 7 |
Note that two of the three are about the user, not about the system. That ratio is unusual among the building blocks in this course, and it is the defining characteristic of client-side monitoring: you are running code on someone else's device.
Key takeaway
Agent plus independent collector solves the coverage problem by reporting from where users actually are, and the collector's independence from the primary service is the load-bearing constraint. Collection is only half of it: millions of reports must be fingerprinted into dozens of issues, ranked by distinct users affected rather than report count, symbolicated through build-keyed source maps or the traces are unreadable and grouping breaks on every deploy, and capped per client and per fingerprint, because error volume spikes exactly when the pipeline matters most.
Interview signal by level
| Level | What a strong answer sounds like |
|---|---|
| L4 | "The client could report errors back to us." |
| L5 | Gets the independence right: "an agent in the client reporting to a separate collector — separate, because if it reported to the main service the report would fail too." |
| Staff+ | Adds coverage and cost reasoning: "this fixes probing's coverage ceiling — reports come from every network that has a user in it. I'd aggregate hierarchically with collectors near clients, stream-process for near real-time spike detection, and sample at around 1%, because we're estimating a rate rather than reconstructing sessions." |
Next: whether the user agreed to any of this.