A bounded telemetry buffer with no notion of priority evicts in arrival order, and arrival order is wrong precisely when it matters: the exception somebody is hunting for arrived before the loop that buried it. That eviction rule is a triage policy, and most of the time nobody chose it deliberately.
Arrival order is a policy nobody wrote down
A render loop throws the same TypeError forty thousand times in nine seconds. The ticket arrives, the console evidence is full, every line is identical, and the ReferenceError that fired once during page load, before the loop started, is not in it.
Nothing malfunctioned. A queue with a fixed size and no ranking has two available behaviours when it fills: discard the oldest to make room, or refuse the newest. Both are decisions about which evidence survives a storm, and both get made by the shape of the data structure rather than by anybody reasoning about bug triage. The default a bounded queue arrives with is an arrival-order rule, and a storm always wins an arrival-order rule, because volume is the only thing it measures.
Deciding differently takes a ranking over kinds of evidence, plus a way to recognise that forty thousand of something is still one fact. Both leave a third obligation behind: saying afterwards what was discarded.
Six bands, and only one direction to evict in
Our automatic buffer sorts the ten categories of evidence capture into six bands. The bands are the policy; the categories are what the capture hooks happen to produce.
| Band | Categories in it |
|---|---|
| 1 | Identity, uncaught error |
| 2 | Network failure, navigation |
| 3 | Console warning, console error |
| 4 | Interaction |
| 5 | Informational log, network success |
| 6 | Replay diagnostics |
When an entry arrives and there is no room for it, the buffer looks for a victim starting at band 6 and walks upward, stopping at the arriving entry’s own band. It never evicts anything it ranks above the arrival, and it repeats the search until the arrival fits or the search comes up empty.
What makes this a ranking rather than a preference is the empty search. The arrival is refused. A successful network response meeting a buffer full of uncaught errors and navigation events does not displace any of them; it is dropped, and the drop is recorded. Arrival order still decides something, but only inside a band, as the tiebreak that picks the oldest of two equals.
Draining works off the same order. A batch is filled band by band, so an upload cut short by its size limit loses the entries the policy already ranked last.
Forty thousand of one error should cost what one costs
Ranking alone does not rescue the ReferenceError above, because the loop’s exceptions sit in a high band too. Volume has to stop competing with variety.
Every entry carries a fingerprint made of its category, its level, its message and its serialized data. An entry whose fingerprint is already present never joins the queue. It updates the entry already there, which gains a running count, the timestamp of the first occurrence and the timestamp of the most recent one. The storm holds one slot and three extra fields, and the report says the exception fired forty thousand times between two moments instead of repeating one line forty thousand times.
The arithmetic stays whole on the way out. When a collapsed entry is eventually evicted, its category’s drop counter is credited the whole count rather than a single unit, so somebody adding up what was captured and what was lost gets the number of events that actually happened rather than the number of slots that were freed.
A reserve one category is allowed to spend
The buffer is bounded in bytes rather than in entries, because an entry is a JSON object whose size depends on what the page threw. Three budgets shape what it retains, and one of them exists for a single category.
| Retention budget | What it bounds | Value |
|---|---|---|
| Buffer total | Every automatic evidence entry held in memory | 2 MiB |
| Fatal reserve | The tail of that total only uncaught errors may enter | 16 KiB |
| Informational sub-cap | Retained bytes of console.log level evidence | 256 KiB |
Three more govern what leaves it. These bound transport rather than memory, and they are the reason a full buffer is not the same thing as a full report.
| Transport budget | What it bounds | Value |
|---|---|---|
| Flush trigger | Buffered bytes that schedule a send | 16 KiB |
| Core batch | Bytes of one ordinary upload | 32 KiB |
| Exit beacon | Bytes of the single send at teardown | 55 KiB |
An uncaught error is admitted against the full total. Everything else is admitted against the total minus the reserve, so the final 16 KiB is unreachable to a flood of successful requests and still available when the page finally throws something nobody caught. The same rule applies when a collapsed duplicate grows: the extra bytes a new count costs are charged against whichever of the two limits its category qualifies for.
Another budget in this widget also measures 2 MiB, and confusing it with the one above is easy. The one above is the evidence buffer: console, network, navigation and interaction entries waiting to be uploaded. The other belongs to session replay, covering the DOM walk in progress plus the chunks queued behind it, and it is described in the hard part of session replay. They bound different things and are checked against separate accounts.
The hole has to be countable
The buffer keeps a running count of what it dropped, one counter per category, and that count travels with the next batch inside the session’s performance health report. The counters use the same ten names as the categories, so a session states how many informational logs and how many successful requests never made it rather than presenting a shorter list that looks complete.
That is the whole of the buffer’s obligation. What a triager should do once the count is on the ticket is a separate argument, and it is settled in the gap in a ticket’s evidence has an owner. Making a gap legible and deciding whose it is are different jobs.
One clamp is worth knowing about. A reported counter tops out at one million per category, and past that a session says a very large amount was dropped without being precise about how much.
The obligation has one hole of its own, at the exit, and it is better stated than discovered. The health report that rides the final beacon is assembled before the buffer is drained to fill that beacon, so every drop the drain itself causes lands in a counter after the number has already been written down. On an ordinary send that costs nothing, because the correction rides out with the batch after it. At teardown there is no next batch and the buffer does not survive the page, so a session that ended with evidence still queued under-reports its own losses by exactly that amount. The counters are honest about the storm and silent about the close.
What the buffer will not do
An entry is bounded before it is ever queued, and the caps differ by what is being copied. Its own message text has a ceiling, every string carried on its data has a tighter one, a stack is allowed more room than those, and the number of keys copied out of that data stops at a fixed count. A cut sets a truncation marker on the entry rather than shortening it silently, so a clipped stack stays distinguishable from a short one, and a reader who finds a short field knows which kind of short it is.
The informational sub-cap catches people out, because it fires while there is still room. Evidence at console.log level stops being admitted once its retained share reaches 256 KiB even when the buffer as a whole is half empty, so a chatty page loses its own logs and keeps its errors.
Plan around the exit, because it is where both halves of the policy run out at once. When the tab closes there is one beacon, and it carries at most 55 KiB and 100 entries. Whatever is still buffered goes nowhere, and the count of it goes nowhere either. The buffer schedules its own send once it holds 16 KiB, so a long visit has usually uploaded most of what it saw, but evidence from the last seconds before a close is the likeliest thing to be missing from a ticket, and the likeliest to be missing without saying so. A defect that only appears during unload is one to reproduce with the tab left open.