Skip to content

feat(rum): add sessionOnErrorSampleRate - #25

Open
Fiona2016 wants to merge 13 commits into
feat/error-session-replay-samplingfrom
feat/error-session-event-sampling
Open

feat(rum): add sessionOnErrorSampleRate#25
Fiona2016 wants to merge 13 commits into
feat/error-session-replay-samplingfrom
feat/error-session-event-sampling

Conversation

@Fiona2016

Copy link
Copy Markdown
Collaborator

Motivation

Stacked on #24, and based on that branch so the diff here is only this change. It should merge after it; GitHub will retarget this at main once #24 lands.

#24 gives an error session a replay that reaches back a minute. Its resource, action and long task detail still starts at the error, which leaves out the failing request and the click that led to it — usually the first two things anyone looks at.

sessionOnErrorSampleRate applies the same idea to the events themselves: a session that is recorded but only stored if it reports an error.

Changes

  • New init option sessionOnErrorSampleRate, drawn only for sessions the plain sessionSampleRate draw missed. Two tracking types are added; existing ones keep their meaning, so sessions in flight are unaffected.
  • Events are held in a buffer between assembly and the send batch. The batch cannot serve as that buffer: ordinary events go straight into a compression stream and cannot be taken back out one at a time.
  • View events are held one per view and kept out of the eviction budget. The backend builds its session row out of them, so a detail released without its view would be unreachable; anything whose view is gone is dropped at release for the same reason, and a view goes once none of its detail is left inside the window.
  • Bounded by time, count and size. When it runs out of room it gives up long tasks and unremarkable requests first, then actions, and errors only once nothing else is left — newest first, so the earliest error, the one that releases the buffer, survives.
  • The release is spread over a few seconds keyed on the session id: correlated errors would otherwise have every client release in the same instant, right when whatever caused them is already under strain. It is flushed early if the page is about to go, and kept when the page is merely hidden, since it comes back.
  • The replay of such a session is withheld alongside its events, whichever replay rate it drew. Until the events are released the session does not exist yet, so a replay sent then has nothing to attach to and is stranded for good if the error never comes. Forcing capture releases both.
  • View events carry sampled_for_error, and detail_sampled_from records where the stored detail begins, so the gap before it reads as data that was never collected rather than data that went missing. It is recorded on the session rather than stamped on the released view events, because the batch upserts views by id and the next ordinary update would replace them first.
  • Withholding stops at the session, not at the page: what a session never released is dropped when it ends, which is also how a withdrawn tracking consent arrives.

Test instructions

yarn test:unit. New specs cover withholding, release ordering, the window, tiered eviction, the container guarantee, hidden pages, session end, and the spread of the release delay.

By hand, against yarn dev:

  1. init with sessionSampleRate: 0, sessionOnErrorSampleRate: 100.
  2. Let the page make requests for a while and watch the network panel: no RUM request is made at all.
  3. Report an error. The requests that follow carry the events from before it, and the view event carries sampled_for_error and detail_sampled_from.
  4. Repeat with the session running well past a minute: only the last minute of detail comes back, not the whole session.

Driven through Playwright against the same page, including a tab switch midway: a session uploaded nothing across 43 seconds, then released 23 requests reaching back 46 seconds, with the marker present on the last view event.

Checklist

  • Tested locally
  • Tested on staging
  • Added unit tests for this change.
  • Added e2e/integration tests for this change.

A session drawn by this rate collects events but uploads nothing until it reports
an error. If none ever happens the session is never stored, and on the first
error the withheld history is released so the detail leading up to the error is
there rather than starting at it.

Events are held upstream of the batch, which cannot serve as the buffer itself:
ordinary events go straight into a compression stream and cannot be evicted one
by one. View events are kept one-per-view and out of the eviction budget, since
the backend builds the session row from them and a detail released without its
view would be unreachable - anything whose view is gone is dropped at release for
the same reason.

The buffer is bounded by time, count and size. When it runs out of room it drops
long tasks and unremarkable requests first, then actions, and never errors. The
release is spread over a few seconds keyed on the session id, because correlated
errors would otherwise have every client release at the same instant, and it is
flushed early if the page is about to go rather than lost to that window.

The replay of such a session is withheld alongside its events, whichever replay
rate it drew: until the events are released the session does not exist yet, so a
replay sent then would have nothing to attach to and would be stranded for good
if the error never came. Forcing capture releases both, for the same reason.
Drops exports nothing outside the module uses, names the entry being appended
instead of reading it back off the end, folds the two ways of emptying the buffer
into one, and records why a view is deleted before being set again.
…r was missed

Two problems with releasing a withheld event buffer.

The jitter meant to spread correlated releases did not spread them. Session ids
are same-length strings over one small alphabet, so summing their character codes
put over 97% of them within 600ms of each other: the herd was delayed by about
two and a half seconds rather than broken up. A multiplicative hash spreads them
evenly across the window, which a distribution test now pins down.

The other is that a session can report its error without the buffer noticing. The
event arrives synchronously, but the state behind it is written through a lock
that can defer the write, so the buffer may still read the session as
withholding, hold the error, and schedule nothing. If the user then leaves - which
is exactly the case this feature exists for - the whole session was thrown away.
The session is now re-read before the buffer is discarded on page exit.
… is evicted 'never'

The size budget measured UTF-16 code units, which understates non-ASCII payloads
by up to three times - a buffer meant to stay inside a beacon could be well past
it before the cap noticed.

The error tier was documented as never evicted, but the eviction loop included it
and took the oldest first: under an error storm the buffer would give up the very
first error, the one that released it and the one the session is about. Errors are
now given up only once nothing else remains, newest first.
…s go

Three lifecycle gaps in the withheld event buffer.

Nothing reacted to the session ending. A release waiting on its jitter was lost
if the session expired first, and a buffer belonging to a session that ended
because tracking consent was withdrawn stayed in memory until some later event
happened to arrive. The session ending is now settled the same way the page going
away already was.

Its stop was never wired into the SDK teardown, so a pending release could still
fire into a batch that had stopped flushing.

Views were kept for as long as the page lived, one per route, which grew past the
detail budget itself and put fifty of them into a release. A view is kept as the
container of the detail hanging from it, so it now goes once none of its detail
is left inside the window - except the view in progress, which is the container
the error will hang from.
…ail marker survive

Two problems the replay side had already reasoned its way out of, which the event
side had not.

The buffer was cleared on any page exit, and a page being hidden raises one -
switching tabs, or switching apps on mobile, wiped the withheld minute and left
an error arriving just afterwards with almost nothing. A page that is really
unloading takes the buffer with it anyway, so there was never anything to gain.
The session ending is different, and still clears it.

The marker saying how far back the stored detail reaches was stamped on the view
events being released, but the batch upserts views by id: the next ordinary view
update, seconds later and without the marker, replaced them before the batch was
ever sent. For the view the error happened in - the one that matters - it never
arrived. It is now recorded on the session, so every later view update carries
it.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant