Conversation
…export from mockProfiler imported registerCleanupTask and getGlobalObject from the rum package rather than core, and profiler.spec.ts imported from package names this repository does not publish. Since mockProfiler is re-exported from the rum test barrel, the broken imports took every spec that touches that barrel down with them - around 220 tests never ran.
A session drawn by this rate records from the start but uploads nothing until it reports an error. If none ever happens, nothing is sent and the session is never stored. On the first error the withheld buffer is released and recording continues normally, so the replay covers what led up to the error rather than starting at it. The buffer is bounded on both axes. Time: a buffer that spans more than a minute is dropped and restarted from a fresh full snapshot, so what is released stays a minute at most. Size: the existing segment byte limit still applies while withheld, and restarts are spaced out so that a document whose full snapshot alone exceeds that limit degrades instead of restarting in a loop. A withheld buffer belongs to the session that produced it. It is released only when that same session reports the error - if the session expires or is renewed first, the records are dropped, so an expiry can never turn into an upload for a session that never errored. Buffers that are dropped roll back their replay stats, and has_replay is not reported while a replay is being withheld, so neither the counters nor the link offer a replay that does not exist. Errors raised by the SDK about its own transport do not release anything: those are our failures, not the application's, and counting them would make every session an error session wherever our endpoint is unreachable.
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.
The flush path derived the same thing twice under two names, and the mapping of the internal checkout reason onto a schema value only ever had one caller.
…sion-event-sampling
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.
A page-exit rotation used to throw the buffer away, and with it the full snapshot a released replay has to start from - everything recorded afterwards is incremental and cannot be played on its own. Switching tabs raises this exit, and the page comes straight back, so an error reported after that would have released a replay that renders as good as nothing until the next view. Nothing can be sent while withheld, so there was never anything to gain from the rotation. A page that is really unloading takes the buffer with it either way.
…sion-event-sampling
…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.
…ased buffer Only the rotation notices that the withheld replay has been released, so a session that expires within one rotation of its own error still loses what the error had earned. Closing it would mean asking the session manager on every record.
…sion-event-sampling
The rollback that gives a dropped buffer's index_in_view back only lands when the encoder finishes, which is always a turn later. Restarting from a fresh full snapshot emitted records right away, so the next segment took its index before the rollback arrived - and once that session errored, two uploaded segments claimed the same index within one view while nothing claimed the first. Any error session that spends a minute on one view before erroring hit it. The restart now happens where the rollback lands. Also corrects a comment: a session expiring right after its own error does not lose the buffer. The history entry is still open when the recorder is stopped, so the stop flush sees the session as released and sends.
…sion-event-sampling
…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.
Without it, a replay collected under this rate is indistinguishable from one collected unconditionally once it has been uploaded - the two cost differently and answer different questions, and nothing downstream could tell them apart.
…sion-event-sampling
The rate a session is drawn on is the console's value falling back to init, with the application's beforeSampling given the last word. That resolution was written inline in the only branch that draws, which is fine as long as a draw is the only thing that needs to know the answer. Move it into a function that resolves and never draws, so the same question can be asked without spending a lottery ticket to find out. No behaviour changes.
Settings published from the console applied to sessions created after
they arrived, and to nothing else. For a visitor who never goes idle
that is hours: a session ends after fifteen minutes without activity or
four hours outright, so the change everyone is waiting on reaches the
people generating the most data last.
Three changes cannot wait, and they are exactly the three whose effect
on the running session can be told without drawing again:
- a session sample rate of 0 while the visitor is being collected;
- a rate of 100 while they are not;
- a stricter defaultPrivacyLevel, where every further second recorded
is a second of plaintext uploaded that masking cannot reach back for.
Each of them ends the current session; the visitor's next action starts
a new one under the new settings. Ending rather than flipping is the
point: the old session is collected to its end as it was begun, so no
replay is masked in one half and plain in the other, and no session is
invented that starts in the middle of a visit.
No other rate says anything about whether THIS session should have been
kept. Only a second draw could, and drawing twice quietly turns a rate p
into p², so every other change waits for the next session — a loosening
privacy level included, where being slow is what leaves room to undo a
mistake.
It needs no bookkeeping to stay idempotent: what it compares is what the
session was drawn under against what a draw would use now, and ending
the session is exactly what makes that difference disappear. The same
response arriving again, in another tab or after a reload, finds nothing
left to act on.
beforeSampling is now called outside a draw as well, to resolve the rate
that would actually apply, so the documentation asks for a callback free
of side effects and stable for the same input.
…ering A const enum's values are inlined at build time and every entry after an insertion shifts, so an entry wedged into the middle of a list that is otherwise upstream's is both a renumbering and a conflict on the next upstream merge. Move it to the end. Also drop a guard that restated its caller's precondition: the event is only ever emitted by the fetcher, which does not exist unless the site opted in, and reading the settings already answers with nothing when it did not.
Marking a session as having reported an error is only useful to a session that is withholding its replay. Doing it for every session wrote the session store for customers who enabled no error sampling at all, and that write also pushes the session's expiry out, which moves where their sessions end.
…take it Flushing a segment always waits for a round trip to the deflate worker, because the trailer is written just before finishing. The collection state is reset synchronously, so a record arriving during that round trip created the next segment while the dropped one was still counted: two uploaded segments then claimed the same index_in_view, and index 0 was never uploaded at all. Each counter is now given back in the phase it was taken in - the segment count synchronously, the record and byte counts in the flush callback.
… current one Assembly resolves a session at the event's own start time, so a request or a view update that finishes after its session ended still carries that session's id. The buffer read whichever session was current instead, which let two things through: a straggler of a session that had ended without ever reporting an error was uploaded on its own - storing the very session the withholding was there to avoid - and one arriving after a renewal was held in the new session's buffer and released by an error that was not its own. A view that already ended no longer becomes the current view when it is updated late either. It carries its own start date, and treating it as current had the pruning drop the view the next error hangs from, so the release filtered that error out of its own buffer.
…sion-event-sampling
Two gaps left by withholding events as well as replays. The mark that releases a buffer was being skipped for a session that withholds only its events, since the check knew about the replay side alone. And a buffer whose session had been renewed into one that withholds nothing was left behind until the session expiry notification arrived, rather than being dropped as soon as the session it belonged to was no longer current.
A session that is not being collected is given no id, so no record of its draw is kept and the privacy level it was drawn under cannot be read back. The comparison fell through to the init value on every announcement and kept answering "tighter", so once an operator tightened `defaultPrivacyLevel` from the console, every sampled-out visitor was put on a loop: end the session, renew on the next click, refetch, end it again. It bought no privacy either -- a visitor who is not collected records nothing, so a stricter level has no plaintext to catch there. The rule now carries its own precondition and applies only while the session is being collected, which is also the only state in which a recording exists. Its fuel was the announcement firing on settings that had not changed: `store()` answered "stored" for a response repeating the version already held, which is the ordinary answer, since every new session refetches and most find nothing new. It now answers whether the stored version actually advanced. Three tests, each checked against the unfixed source first: a sampled-out session is left alone when the level tightens, it is still left alone as further settings arrive, and a response repeating the stored version is not announced.
Three paths the implementation documents had no test standing on them, each found by mutating the source and watching the suite stay green: - A session drawn before any settings arrived. A draw that lands exactly on the init values records nothing, so the level such a session runs under can only be read back off init -- the fallback every existing privacy test stepped around by storing settings before starting. Deleting that fallback passed the whole suite. - A response that carries no rate at all, with `beforeSampling` turning the delivered custom values into the decision. This is the "called away from a draw" contract, and both resolving the rate without the callback and bailing out when the console sends no rate passed the whole suite. - The console's kill switch, which stores a version and nothing else and so puts the rates back to the ones init passed. That is a change like any other, and where init never collected it is the decisive one. Also renames the opt-out test to what it actually pins down. Its store key is one no implementation could derive, so it cannot witness the store being left alone; what it does witness is the decision surviving an undefined `remoteConfig`.
feat(rum): apply a rate that leaves zero to the running session
…on-event-sampling
…on-event-sampling
endSessionIfSettingsAreDecisive ends a running session when the settings just delivered resolve the session sample rate to zero. With sessionOnError a zero rate is the switch's ordinary setting rather than a stop: at a zero rate a session is tracked exactly when the switch is on (a replay-on-error switch cannot keep one on its own, the session draw fails first). Ending it there discarded the very session the switch exists to keep and left the page blind from the first configuration fetch - which lands on every fresh profile and after every deploy - until the visitor's first interaction, so page-load errors were never captured. Gate the expire on the switch being off: a zero rate still stops plain sessions, and one turned off from the console still stops on-error ones, but a zero rate beside an on switch no longer ends the session. Add specs for both directions.
…d branches - startSessionReplayRecordingManually's doc still stated the pre-switch default rule; describe the derived default that also accounts for sessionReplayOnError and remoteConfigurationEnabled. - The mark guard comment said "neither rate"; both are switches now. - Add specs for two withheld-buffer branches that no test reached: a page reactivation must not cut the withheld buffer, and the SESSION_RELEASED subscription must wake the deferred snapshot restore with no rum event.
…ng' into feat/error-session-event-sampling # Conflicts: # packages/rum-core/src/domain/configuration/configuration.ts
…ive spec gaps - Add a changelog entry for sessionOnError / sessionReplayOnError. - The setSessionHasError doc and the error-tracking doc said the mark only releases a withheld replay; it now also releases withheld events. - Type the three fork-added session marker fields at the assembly site so a misspelled key fails the build instead of the schema's index signature quietly accepting it. - Cover five branches no spec reached: the console turning sessionOnError off, tiered eviction keeping an older action over newer long tasks, an event collected after the release flowing through instead of being held again, sampled_for_replay staying off for an error-replay session that has not errored, and the manual-start warning staying silent when replay is off.
…oversized-error release - An on-error session (kept by sessionOnError despite the plain draw missing it) now reports session_sample_rate 0 instead of the plain rate. The console extrapolates stored sessions by 100/session_sample_rate; reporting the rate that missed the session had each error session counted as 100/rate sessions. Reported after the draw ladder so the tracking type is known; a type that only withholds its replay still reports the plain rate it was drawn under. - The never-applies / no-session-tracked init warnings no longer fire under remoteConfigurationEnabled, where the init rates are a fallback the console can override - they were false-positiving on the documented remote-config setup that omits the rate. - A releasing error larger than the buffer budget now forwards on its own and schedules the history's release behind the jitter, instead of releasing the whole history in the same tick and defeating the anti-thundering-herd spread for the correlated outage the jitter exists for.
…nd two buffer edges - The zero-rate emergency stop now spares only the session that is itself an on-error one (withholdsEvents its type), not every session while the switch is merely on: a plainly drawn session is still ended by a rate-0 publish and redraws as on-error next action, instead of continuing to upload in full. - beforeSampling returning 0 for a rate now also clears the matching on-error switch, so the documented "0 never collects" is not quietly turned into "collect on error" for an excluded visitor. - A session whose type an older shared-cookie bundle rewrote under the same id is no longer blacklisted: its withheld buffer is dropped, but its events go on uploading as the plain session it now is. - A single non-error event larger than the whole buffer budget is dropped rather than held, so it cannot evict the minute of history to make room it could never fit into.
Record three known limitations of sessionOnError / sessionReplayOnError in the changelog, and note the replay's view-boundary in the sessionReplayOnError doc: consent-gated recording must set startSessionReplayRecordingManually explicitly; the released replay reaches back only to the error's view while events reach back the full minute; and under the opt-in compressIntakeRequests a tab close within seconds of the first error can lose that release.
Resolves the two conflicts with the "a rate that leaves zero" work already on publish: the zero-rate emergency stop keeps its on-error guard, so a rate of 0 spares only a session that is itself on-error and still ends a plainly drawn one; the not-tracked branch also ends a session when the switch turns on at rate 0; and the CHANGELOG Unreleased block sits above v0.2.3.
…der the same id The same-id guard added in 9eef19d only speaks on the event path, but the store poll always speaks first: it expires the session on the type change, and the expiry discards the buffer with the blacklist on. The session is then renewed under the SAME id by the store, and every event it collects is dropped for the rest of its life - a session the backend goes on storing and billing. Enforce the blacklist only against a session that is not the current one: a blacklisted id that is nonetheless live can only come from such a foreign same-id redraw, while a session that truly ended comes back with a new id, so real stragglers are still dropped. Cover the actual flow (poll expiry, then same-id renewal) with a spec.
The lock field fell into the carry-unknown-fields path: every access rewrote the cookie with the lock kept and the expiry renewed for a year. The modern bundle has no stale-lock recovery, so a lock whose owner was gone (a crashed tab, or one whose write we raced) stayed alive for as long as a legacy page kept the cookie warm - wedging the modern session store: every write retried and dropped, the in-memory session never expiring, and every new page's init failing on an empty cache. Dropping the field at parse time turns the legacy rewrite into a stale-lock cleaner, and a legacy write that lands inside the modern lock window now fails the modern corruption check (a retry) instead of passing it with the session silently rolled back.
A session kept only because it errored reports a session sample rate of 0, so that it counts as one session instead of `100 / rate` sessions. That 0 was stored with the draw record, which lives in a single per-origin storage slot, while the on-error tracking type lives in the session cookie. A page load that restored the session without finding the record - storage cleared, or the next subdomain under `trackSessionAcrossSubdomains` - fell back to the init rate, and the session was extrapolated as `100 / rate` sessions again. Derive the 0 from the tracking type when the event is assembled, and keep the draw record to the rate actually drawn. The legacy bundle, which collects released on-error sessions from the shared cookie, reports 0 for them as well.
Only a session whose events are withheld until an error stands for itself. A session that withholds only its replay was kept by the plain session draw and must keep reporting that rate. Cover both on-error replay tracking types in the session context, and note the exception where the drawn configuration is documented.
…session A session kept only because it errored lost two things on the way out. The error that releases a withheld replay was assembled while the replay was still withheld, so it never claimed one, and neither did the events released with it: the replay was uploaded, but the error it was kept for did not point to it. The error now claims the replay when it marks the session, and each released event claims the replay its view actually kept. A release at page exit is larger than what a browser guarantees to send while the page goes away, and only its first requests are sure to leave. Views went first and errors last, so a user leaving right after the error could store a session without its error. Errors now follow the views, ahead of the older events. The changelog now describes the page-exit limit for any release, not only compressed ones, the snapshot cost of a withheld replay on busy pages, what a withdrawn consent still uploads, and that a console Session Replay rate now starts the recorder when init passes a rate of 0 with remote configuration on.
…pt one The releasing error claimed a replay whenever the recorder was running. A withheld segment that is dropped gives its records back, so a view can have none left while the recorder keeps going, and the error then pointed to a replay that was never uploaded. Judge the claim by the records the error's own view still holds, like the events released alongside it.
Merge error-session capture onto publish
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
How it got to
publishThrough two pull requests, no direct commits:
was stated.
What 0.2.1 contains
Settings published from the console apply to sessions created after they arrive. A visitor who
loads a page downloads the new settings within seconds, and until now the SDK filed them away and
went on under the old decision for the rest of that visit — up to four hours. Two changes no longer
wait, because their effect on the running session can be told without drawing again:
defaultPrivacyLevelgets stricterBoth are gated on the session being collected: a visitor who is not records nothing and sends
nothing, so neither rule has anything to act on for them. Ending the session, rather than flipping
the running one, is what keeps a replay from being masked in one half and plain in the other.
A rate rising to 100 could be asserted about an uncollected session and is deliberately not acted
on:
setForcedSession()already covers "collect this visitor now", it is the one direction thatraises volume unannounced, and nothing about it is urgent.
What it is worth, honestly
Settings are fetched at start-up and on session renewal only, never on a timer. So this is worth
exactly as much as the client's chance to hear the news: seconds for a visitor who keeps loading
pages, and nothing at all for a single always-visible tab until the four-hour session cap — the
visibility timer keeps renewing that session, so it never goes idle and never refetches. Any other
tab the same visitor loads ends the session they share.
Where to look first
packages/rum-core/src/domain/rumSessionManager.ts—endSessionIfSettingsAreDecisive, the ruleitself: nineteen lines, no nesting.
packages/rum-core/src/domain/configuration/remoteConfiguration.ts—store()now reportswhether the response advanced what is stored, and the fetcher announces only when it did. An
answer repeating the stored version is the ordinary answer and must stay silent.
CHANGELOG.md— the## Unreleasedsection, which leads with the symptoms that will read like aregression on publish day.
State
lerna.jsonis still0.2.0and the changelog section is still## Unreleased: nothing isreleased. Cutting 0.2.1 is the version bump, the lockfile refresh and the
v0.2.1tag, which iswhat triggers the npm publish.
Local and CI checks were green on both #40 and #41:
format,lint,typecheck,build,check-packages,test:compat:tsc,test:compat:ssr, and the unit suite at 2984.