Repository navigation
✨ add session sampling and error-session capture (sessionOnError) - #29
Merged
Merged
Conversation
Add `sessionSampleRate` (0-100, default 100; an out-of-range value fails init) and `sessionOnError` (default false). Each session is drawn once in the main process and keeps its decision when resumed: drawn sessions report as before, sessions not drawn report nothing, and with `sessionOnError` the sessions the rate missed are held in memory and uploaded only once they report an error. - SessionManager draws the tracking type and persists it, with the error mark, in `_dd_s` and in the session history. - SessionContext discards the events of sessions not drawn and the late events of withheld sessions that ended without an error, and stamps `session.sampled_for_error` on views and `_dd.configuration.session_sample_rate` (0 for on-error sessions) on main-process and renderer events alike. - WithheldEventBuffer sits between assembly and the RUM batch: last 60s, 64 KiB / 200 events plus 50 views, tiered eviction, release ordered views / errors / rest behind a per-session jitter of up to 3s, frozen window once scheduled, oversized releasing errors sent on their own, discard on session end, immediate release on session end, quit and uncaught main-process exceptions. Telemetry bypasses it. - A native crash reported on the next launch releases its withheld session and brings its view, rebuilt from the view history. - The bridge answers no session id for sessions not drawn, so renderers record nothing for them. - Telemetry gains a debug log, used to report each release.
Scenarios against the fake intake: nothing uploaded for a withheld session that never errors; release of main-process and renderer history with the on-error markers; renderer errors dropped by beforeSend do not release; discard on session end; release at once on an uncaught exception; native crash of a withheld session reported with its view; and the negative controls (drawn session unmarked at its rate, undrawn session uploads nothing and renderers see no session).
- SessionManager writes its state through one promise chain, so a slow activity write can no longer land over the error mark; the current session's draw and mark live in a single copy. - WithheldEventBuffer loses its unused stop(); an oversized error is measured once. - RumCollection hands the ViewContext to ViewCollection and to the crash path directly, sharing its MainView type.
Accept extra released event types such as a renderer long task, and give the scenarios, which sleep past the release jitter, a longer budget.
…release before returning - Mark a withheld session's error on the history entry in force at the time of the error, plus the active entry when the session is the current one, rather than on every entry of the session. An error of the current launch no longer makes a native crash of the previous launch look already released, which left the crash without its view and dropped it at release. - Chain the session file delete behind the queued state writes, so a save queued just before the session ended cannot recreate the file and resume an ended session on the next launch. - Write the release triggered by an uncaught exception or a quit before returning (BatchProducer.postSync): the queued write never ran when the host exited from its own uncaughtException listener. - Never evict the view that holds an error past the view limit: the error was dropped at release for want of its view. - Persist the rate a session was drawn at and report that one, so a session resumed under a changed configuration, or saved before sessions were sampled, is not extrapolated by a rate that did not draw it. - Attribute the telemetry of a session the draw did not keep to no session, as the browser SDK does. - Reword the sessionOnError warning, and the README on what an exit does.
…on only once it is released - Move exit-time durability from the withheld buffer to the transport: the batch producer keeps what is posted in a queue, and on APP_MAY_EXIT (an uncaught exception, or now a quit) Transport writes the queue before returning. The buffer's own synchronous sink is gone with it. An expiry release queued just before a quit, an oversize error forwarded on its own, and the fatal error of a drawn session are no longer lost to the exit. - Name batch files strictly increasingly: several rotations within one millisecond shared a name, and the later .log replaced the earlier one. - Mark the session as errored when its buffer is released, not when the error arrives: the mark is what tells a crash reported on the next launch that the crashed view reached the batch, and before the release it had not, so the crash was reported without its view. - Never evict the view just updated past the view limit: the view a crash brings along arrives before the crash, and was evicted by the time the crash needed it. - Write the session file only while the session is active, and not from an activity read that completed after it expired: either write brought an ended session back on the next launch. - Close the previous launch's history entry on startup, resumed session or not: an open entry is never pruned, and the file grew with every launch that resumed its session.
…ts view A detail whose view was no longer held — evicted past the view limit, or a renderer event dated in the previous session while the renderer had not yet noticed the renewal — was dropped at release. An error session must not lose its error or its history; the views only order the release. An ended view with no detail left is still pruned.
… is issued - An event left the batch queue before its append was issued, while the directory check or a rotation was still awaited; an exit flush in that window found nothing to write and lost it. It now leaves the queue only as the append is issued, and the queue skips it if the exit flush wrote it first. - Orphan recovery never renames a .tmp onto a .log that exists: an append still in flight could recreate a .tmp a synchronous rotation had just renamed, and the next launch replaced the earlier batch with it. - For a crash of a session that was resumed and is current, leave the mark to the release of what this launch holds, like any other error: a mark written while the rebuilt view still waited in the buffer told a second crash that the view had reached the batch when it had not. - Run upload cycles one after another instead of skipping a flush while a cycle runs, so that flush() resolves only once what it was called for is uploaded; the e2e scenario no longer pads it with a sleep.
… and never share a file between the two writers - An event that cannot be serialized (a BigInt in an error context, say) rejected the batch queue's chain for good, and a failed upload cycle did the same to the upload chain: nothing was written or uploaded after it. The event is dropped like a failed write, and a failed cycle is reported to the flush that ran it while the next cycle runs regardless. - A large event is appended in several chunks; a line the exit flush wrote to the same file between two of them corrupted both. The exit flush now starts a new batch while an append is in flight and leaves that file to the append, which rotates it once done. - A size rotation that completed after the exit flush had rotated the same batch and started a new one reset the batch state, leaving the new batch an untracked .tmp until the next launch. The reset now applies only when the batch rotated is still the current one. - Telemetry formatted an error before checking whether it would send it, so a sampled-out telemetry could throw on a value it cannot serialize from inside an error path and lose the error being reported. - The main process's sampling marker is assigned on bridged events rather than merged, so one a renderer supplied is replaced, absent included.
…its uncaughtException listener The existing uncaught-exception scenario keeps the process alive and flushes, so it passed with the exit-time write removed. The new scenario ends the process from the application's own listener, registered after the SDK's, and reads the batch files off disk: the held history and the fatal error are there, and nothing reached the intake.
…ith the exit flush - The withheld buffer measured events with browser-core's byte count, which takes an ASCII fast path and otherwise reaches for window.TextEncoder — undefined in the main process. Holding an error whose text was not ASCII threw, the session was never released, and an exit then had nothing to write. Bytes are counted with Buffer. - The exit flush wrote the released batch synchronously but left the session's error mark to an asynchronous write, so a host that ended the process from its uncaughtException listener left the next launch resuming the session as withheld again. The flush now writes the session state and history too, and the history is serialized when a write runs rather than when it is queued, so an earlier queued write never lands an older state. - The view of an error forwarded on its own for exceeding the budget had no protection against eviction past the view limit; a renderer view arriving during the jitter could push it out before the release. - The synchronous batch rotation now also avoids a .log that exists, as a clock set back can make a new launch reuse an earlier launch's name. - The main view counts only its own events: a crash reported on the next launch is rebuilt with its own view and count, and was counted twice. - Docs: an append already issued is not completed by the exit flush; the application's uncaughtException listener must be registered after init. - Tests: the host-exit scenario asserts the child's exit code; mock implementations no longer leak across cases; capped telemetry case.
…s file, and age the buffer by a monotonic clock - The exit flush wrote the session state and history in place, where a write of the same file already issued could overwrite or interleave with it. Both are now written to a file of their own and renamed into place. - A session ended with stopSession() right before a fatal exit kept its file: its delete was queued behind the writes. The exit flush deletes it. - The view counter guard read the event's view before its type; a telemetry event assembled after the main view closed has none, and reporting an SDK failure threw. The type decides first. - The withheld buffer aged its events by the wall clock, so a clock correction before the error pruned the minute it promises. It ages by the monotonic clock, as the browser SDK does.
… of its own A native crash is reported on the next launch and attributed to the view in force when it happened; when that view was no longer in the view history, the crash was discarded at assembly for want of a container, and a withheld session was left unreleased on top. A session must not lose its crash: the session is released regardless, and the crash goes out under a view id of its own, with the session's markers and a rate of 0, when none can be rebuilt. The RUM hooks take the view a raw event names itself as a fallback to the one in force at its time.
…along with the exit flush - The exit flush ran on before-quit only. An error reported after the SDK's listener — by a listener registered later, during will-quit, or in the same turn as process.exit() or app.exit() — stayed behind its release jitter and was lost. The flush now runs on before-quit, will-quit and the process exit event, each pass taking what arrived since the one before; the exit event is what process.exit() and app.exit() still run. - Telemetry bypasses the withheld buffer by provenance now, not by its type field: the SDK's own telemetry is assembled without a source, while a renderer's event carries one whatever it calls itself. - A flush rotating the current batch detaches it before awaiting the rename: a synchronous write meanwhile opens a new batch instead of recreating the file the rename is taking away, which left it an untracked .tmp until the next launch. - The synchronous write creates the track directory once, not per item.
… read back - The session state and history files were written in place by the asynchronous path, whose open() is itself asynchronous: a write issued before a synchronous exit-time replacement could open the replacement and truncate it. Every write now goes to a temp file renamed into place, and every write and delete carries the generation it was requested at: one lands only if nothing requested later has landed since, so an earlier write or delete can never undo a newer snapshot. Temp files an earlier launch left behind are swept at startup. - A saved session state or history record is validated when read: a non-empty id, a known tracking type, a finite rate, a boolean error mark, finite timestamps. A malformed record is treated as absent — a fresh session is drawn, a history entry resolves no session — rather than resumed under a draw that was never made. Records written before sessions were sampled are still read as drawn.
…ields defensively - A renderer envelope is forwarded only when its event is one of the RUM event types the intake accepts; one claiming to be telemetry, say, is neither a RUM event nor the SDK's own telemetry, and is dropped with a telemetry error naming the type. The withheld buffer no longer throws on an event that lacks its error, resource or view fields: an error without an error object still releases, a resource without one is a success, and an event without a view is held and released under none. - The error that earned a release is never evicted, even when the buffer holds only errors that did not earn one.
…, and the sampling messages The README says which exits take a late error along (before-quit, will-quit, the process exit event that process.exit() and app.exit() run), that the buffer keeps up to 60 seconds of what preceded the error subject to its limits, and that sessionOnError keeps undrawn sessions in memory. The initialization messages state the consequence of an invalid rate and that a resumed session keeps its draw.
The exit flush ran on the process exit event and then returned; an error a later exit listener reported — the application's own, registered after init() — scheduled a release that no turn of the event loop would ever run. The exit event now puts the transport in a terminal mode: the withheld buffer releases as a release is earned, and every event is written before returning. A batch write that throws on exit no longer skips the session state write, which is the one the next launch reads. The batch queue's drain chain settles after an unexpected throw, reporting it, instead of leaving every later write queued behind a rejection. BatchManager.flushToDisk() drains and rotates without uploading, for a caller that needs its events on disk and nothing more.
…red crash at once A crash recovered on the next launch joined the withheld buffer behind the release jitter while its dump was deleted as soon as it was parsed, so a second crash inside that window lost the first for good. A recovered crash now releases the session at once — its upload is spread by the batch cycle already — and the dump is deleted only once the report is in a batch file; a dump whose report could not be written is kept for the next launch. The buffer's byte count no longer throws on an event that cannot be serialized: it counts as larger than any budget, so a releasing error of that kind is forwarded on its own and the history still releases. A view update larger than the budget is dropped, the version held before it keeping the view's place.
A saved state with a null tracking type, a rate without a type, or an error mark without a draw was read as a pre-sampling state and resumed as collected. Only a state with none of the sampling fields is a legacy one; anything else must be a complete record, with a rate from 0 to 100, or a fresh session is drawn. An activity that reads the session file while a renewal's delete and write are still queued sees the session before: that is the renewal's own race, skipped silently rather than reported as an invalid state.
The intake decodes every number but a known few as an integer and fails the whole event on a fraction, silently. A renderer error carrying one — a fractional date, say — released a withheld session and was then never stored. Such an event is dropped at the bridge and reported through SDK telemetry, so it can neither upload nor earn a release. Telemetry about dropped renderer input no longer quotes the input: the unsupported-type message names no type, and the parse-error message carries at most 200 characters of the payload.
…error the intake would drop The host reports an error from its own process exit listener, registered after the SDK's, and the batch files on disk carry it along with the held history. A renderer error with a fractional date neither uploads nor releases the session, while a well-formed one does.
…lush can and cannot take along The README says a session that never errors uploads nothing, not even at quit; that the buffer stays in memory while the session state and view identifiers are persisted for crash attribution; that once the exit event has started every event is written as it arrives, that an asynchronous append already in progress is not completed by the drain, and that a listener registered before init() which ends the process synchronously pre-empts the SDK's.
A renderer envelope with an own `__proto__`, `constructor` or `prototype` key passed the bridge's type check and was merged into the main-process event by `combine`, which copies own keys: `__proto__` reached `Object.prototype`, for every object in the process. The bridge now drops such an event whole and reports it through telemetry. Dropped rather than stripped — nothing honest sends one, and an event with a hole where the renderer put something is an event nobody asked for.
…gers
The fractional-number check on renderer RUM events treated every number
outside a list of field names as one the intake decodes as an integer, so an
error carrying `context: { price: 19.99 }` — valid, the intake stores
`context` as it is — was dropped, and with `sessionOnError` could not release
its session. The check now follows, per event type, the paths the intake's
event schema types as integers; free-form attributes (`context`, `usr`,
`feature_flags`, custom attributes) are never inspected. The e2e intake's
protocol check keeps the same table.
The error reported when a bridge message could not be parsed carried the first 200 characters of the message; telemetry leaves the device, and that text is the renderer's. It now reports only the message's length.
…s kept `BatchProducer` swallowed an `appendFile` failure to keep its queue going, so `flushToDisk()` resolved with nothing written and crash processing, trusting it, deleted the only copy of the dump. The queue still goes on and the batch is still rotated, so what was written uploads; the next `flush()` then rejects with the first failure, and crash processing keeps the dump for the next launch.
The check on renderer RUM events rejected a fraction at an integer path but let any other value through — a string `"500"` at `error.resource.status_code`, say. The intake refuses anything but a whole number into an integer and drops the event silently, so such an error was accepted here, released its withheld session, and never landed: the session uploaded without the error that earned it. Every value at an integer path must now be a safe integer; absent and `null` stay accepted, as the intake decodes either as zero. The values of `view.custom_timings`, a map the intake types as integers, are checked too. The e2e intake mirrors the rule and the contract scenario pins it.
The scan ran on the RUM event only; a key outside it, on the envelope, got through. Nothing read those keys, but the rule is the whole message: it now runs on the parsed message before anything looks at its type.
…buffer The architecture document still named the fork's upstream as the destination, and its diagram sent telemetry through the withheld buffer, which the text beside it says telemetry bypasses.
Every event type declares its own `_dd`, which shadows the common one; the view's has no `session`, so the intake never decodes `_dd.session.plan` on a view event and accepts anything there. The path was listed as common to all types, so a view carrying a fraction there was dropped here for nothing. It now belongs to the five types whose own `_dd` declares it. The e2e intake mirrors the move.
Merged
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.
Summary
Adds session sampling and error-session capture to the Electron SDK.
sessionSampleRate(0–100, default 100): sessions are now drawn in the main process. The default keeps the existing behaviour, where every session is collected.sessionOnError(defaultfalse): sessions the draw did not pick are still collected, but held in memory in the main process. They are uploaded only if the session reports an error: the last 60 seconds of main-process and renderer events are released once after a 0–3 s per-session delay, then collection continues normally. Sessions that never error are discarded.beforeSendnever arrive.before-quitand on a main-process uncaught exception._dd.configuration.session_sample_rate: 0, and their views carrysession.sampled_for_error: true.Known limitation
Held sessions still upload renderer Session Replay when
sessionReplayDirectUploadis enabled, because direct upload bypasses the main process. The README advises against combining it withsessionOnError.Testing
sessionOnErrorscenarios with negative controls:beforeSenddropping the error → nothing;uncaughtExceptionlistener → history and fatal error on disk, nothing at the intake;process.exit(), from awill-quitlistener, in the same turn asapp.exit(), and from the host's own processexitlistener → on disk;context→ uploaded as it is, releasing the session; a renderer error with its status code as a string → neither uploaded nor releasing the session, while the same error with a number does;Behaviour change for all users: view error counts and native crashes
The main-process view's
error.countnow counts only errors that belong to that view. Before, every main-process error incremented the counter of whichever view was current when the error was reported, which is not the same thing for a native crash: a crash is reported on the launch after it happened and belongs to the view that was running then, yet it bumped the new launch's view — along with itsdocument_versionand a scheduled view update — while the view it belonged to never got the count. With this change the crashed view carrieserror.count: 1(rebuilt from the view history, or reported under its own view id when that history is gone) and the current view's count reflects its own errors only. This applies regardless ofsessionSampleRateorsessionOnError: any application that reports native crashes will see the new launch's first view stop showing an error it did not have, and the previous launch's view show the one it did. Dashboards that summed crashes off the current view's counter will see them move to the view the crash occurred in. The crash error event itself is unchanged.Behaviour change for all users: renderer event types and exit flush
before-quit,will-quitand the processexitevent. An error reported while quitting, or in the same turn asprocess.exit()/app.exit(), therefore reaches disk and is uploaded on the next launch.Behaviour changes for all users: exit, recovery and renderer input
exitevent has started, every event is written to disk as it arrives — an error reported from the application's ownexitlistener, registered afterinit(), reaches the batch file too. Before that point the flush runs onbefore-quit,will-quitand the start ofexit, as described above.date, say) is dropped at the bridge and reported through SDK telemetry, since the intake would drop it silently; such an error can no longer release a withheld session. Telemetry messages about dropped renderer input no longer quote the input.nulltracking type, a rate without a type, a rate outside 0–100) starts a fresh session instead of resuming as collected.Behaviour changes for all users: renderer input validation and crash dump retention
__proto__,constructororprototypeanywhere — in the event or around it — is dropped at the bridge and reported through SDK telemetry: merged into an event, such a key reaches the prototype of every object in the main process.date, durations, counts, timestamps, the values ofview.custom_timings, per event type — taken from the intake's own schema): every value there must be a whole number — a fraction, a string, a boolean or an object is refused, as the intake refuses it; absent andnullare accepted, as the intake decodes either as zero. Free-form attributes —context,usr,feature_flags, custom attributes — are never inspected, so an error withcontext: { price: 19.99 }is accepted and uploaded as it is, and releases a withheld session like any other error.userDatavolume) keeps the dump for the next launch to report, and is reported through SDK telemetry.