A bounded buffer is a triage policy

A telemetry buffer with no ranking evicts in arrival order, which is the wrong order when the evidence somebody needed arrived before the storm.

Six smoked-glass evidence trays form two tiers, with one priority compartment glowing orange on black

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.

BandCategories in it
1Identity, uncaught error
2Network failure, navigation
3Console warning, console error
4Interaction
5Informational log, network success
6Replay 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 budgetWhat it boundsValue
Buffer totalEvery automatic evidence entry held in memory2 MiB
Fatal reserveThe tail of that total only uncaught errors may enter16 KiB
Informational sub-capRetained bytes of console.log level evidence256 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 budgetWhat it boundsValue
Flush triggerBuffered bytes that schedule a send16 KiB
Core batchBytes of one ordinary upload32 KiB
Exit beaconBytes of the single send at teardown55 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.

Common questions

Why does a bug report show thousands of identical errors and not the one I need?

A fixed-size buffer with no ranking keeps whatever arrived most recently and discards whatever arrived first, so a loop emitting one exception thousands of times can evict the unrelated error that fired before it. Collapsing repeats into a single counted entry is what stops that.

What is priority-aware backpressure in a telemetry buffer?

Priority-aware backpressure means the buffer picks its victim by how useful the evidence is rather than by when it arrived. Ours sorts ten evidence categories into six bands and always looks for room in the least useful band that still holds an entry.

How are duplicate console errors handled in a captured session?

Repeats are matched on a fingerprint built from the category, the level, the message and the serialized data. A match updates the entry already stored with a running count, the timestamp of the first occurrence and the timestamp of the latest one.

Why would evidence be missing from a bug report even when nothing crashed?

Capture budgets are finite, so some evidence is refused admission or evicted to make room for something ranked higher. The session carries a running count of what was dropped in each category, which leaves the gap readable instead of leaving the log looking complete.

What happens to buffered evidence when the reporter closes the tab?

One exit beacon goes out carrying at most 55 KiB and 100 entries, drained in band order so the most useful evidence fills it first. Anything still buffered after that is counted as dropped, but the beacon's health report was assembled before the drain and no batch follows it, so that last count never reaches the server.

Does an uncaught exception get treated differently from a console error?

Yes. An uncaught exception shares the top band with identity evidence, and it is the only category allowed to spend the 16 KiB reserve held back from the total budget. Every other category is admitted against the total minus that reserve.

The Notarus team

The people who build Notarus. We write up the decisions behind the product when the reasoning is worth more than the changelog line.

Keep reading

Reports with the evidence attached.

Start free, one script tag.

<script src="…/widget.js" data-key="pb_pub_…">