Skip to content

✨ add remote configuration for the session sampling - #30

Open
Fiona2016 wants to merge 28 commits into
feat/session-on-errorfrom
feat/remote-config
Open

Fiona2016 wants to merge 28 commits into
feat/session-on-errorfrom
feat/remote-config

Conversation

@Fiona2016

@Fiona2016 Fiona2016 commented Oct 9, 2026 •

Copy link
Copy Markdown
Collaborator

Stacked on #29. The base is feat/session-on-error. Once #29 merges into publish, retarget this PR to publish.

Summary

Adds RUM remote configuration (dynamic config delivery) to the Electron SDK. Before this change the SDK had none.

  • New init option remoteConfigurationEnabled. Defaults to false. When it is off, nothing changes: no requests, no file reads, and only the init values apply.
  • Fetching. The main process fetches GET /api/v2/rum/config with client_token, sdk=electron, sdk_version, env, app_version and applied_version, plus If-None-Match with the stored ETag. Behind proxy, the parameters ride inside ddforward.
  • When it fetches. At init and at every new session, with no timer in between. When the console sets refresh_on_foreground (off by default, since it bunches requests when a fleet comes back at once), the SDK also fetches when a window of the app gains focus (app browser-window-focus), provided the last completed request, successful or failed, finished at least ttl ago (60 s floor, 600 s default). These requests are conditional, there is never more than one in flight, and a focus refresh neither cancels a pending retry nor starts a new round of retries. The permission and ttl are kept in the cache file, so they survive restarts that only ever see a 304.
  • Size bound. An answer is read as a stream, and only for a 200. Past 256 KiB the rest is cancelled and the answer is treated as malformed: the last good configuration is kept and the request is retried. A cache file larger than that is not read. custom values nesting deeper than 16 levels (the backend allows 3) make an answer malformed and a cache file unreadable, and a serialization failure at exit is reported rather than thrown, so stopping always aborts the request, clears the retry and removes its listeners.
  • Retries. Network failures, timeouts, malformed bodies, 429 and 5xx are retried with successive delays of about 5 s and 60 s, each with ±20% jitter. That budget is re-armed by a new session and by any answer that lands; once a run of failures has spent it, a foreground refresh makes one attempt with no retries behind it. An unexpected 2xx (a 204 from a proxy, say) is treated like a malformed answer and retried. Other 4xx responses and an unknown schema are not retried.
  • Cache. The last good configuration is cached on disk (_fc_remote_config) as an atomic state file, keyed by endpoint, application, env and app version. It is used before the network answers and while offline. A cache file written by another SDK version keeps its values but drops its ETag, so the first request after an upgrade is unconditional.
  • What it applies. rum.sessionSampleRate and rum.sessionOnError: a remote value overrides init, and an absent or invalid knob keeps init. custom is exposed through getRemoteConfig() as a structuredClone copy, so keys such as __proto__ stay plain keys. A version 0 answer, or a version 0 record on disk, is treated as no configuration, whatever it carries. Lower versions are refused as late answers: the backend allocates versions as MAX(version)+1, republishes rollbacks under new numbers and never prunes the latest.
  • Activation.
    • next_session never touches the running session.
    • immediate ends a collected session when the rate becomes 0, unless it is an on-error session and the switch stays on. A session drawn out at rate 0 is redrawn when the rate leaves 0 or the switch turns on. A draw lost at a positive rate keeps its outcome.
    • At startup, a kept immediate configuration judges the session the launch starts with, before anything is collected. Same and older versions (or none) are judged, which also covers a session drawn under the same version as read by an older SDK; a session drawn under a newer version is left alone, since the newer configuration's write may be the one that was lost.
    • The same guard applies to immediate answers from the network: versions are published in order, but a backend node can serve an older snapshot for a while, so an older immediate answer never ends a session drawn under a newer version. The same version is still judged.
    • An immediate configuration is also enforced when a same-version response changes values, which happens after an SDK upgrade.
  • On exit. A configuration whose write has not landed, or failed, is written synchronously on every exit notice and when the app quits, which covers app.exit(). It stays pending until a write actually lands. Pending requests, timers and listeners are then stopped.
  • Session lifecycle. A session ended by an immediate configuration while or after its state was written gets no timers and no renewal notice: the renewal is announced in the same continuation as the last check that the session is active, so nothing left behind can end a later session early.
  • Version reporting. _dd.configuration.rc_version is reported on the events of sessions drawn under a delivered configuration. The drawn rate and version persist with the session across restarts. An application with no published configuration (version 0) reports neither rc_version nor applied_version.
  • Renderers. Over the bridge, the browser SDK does not fetch, and the main process sets rc_version on renderer events.

Testing

  • vitest: 1029 tests, all pass (on the branch merged with feat/session-on-error). Typecheck, e2e typecheck and prettier pass. Lint reports only the two pre-existing errors.
  • Playwright e2e: 93/93 pass on the merged branch (88/88 twice before the merge), against the local fake intake, which serves /api/v2/rum/config the way the backend does, including version 0 for applications with no published configuration. Every scenario has a negative control. The scenarios cover:
    • rate 0 with the switch on and immediate activation;
    • an emergency stop, compared against next_session;
    • the switch turned on for a session drawn out at rate 0;
    • deferred activation;
    • a cached configuration used offline on a cold start;
    • an immediate stop enforced on a resumed session after a restart;
    • a post-upgrade same-version response;
    • a foreground refresh that delivers an immediate stop on focus once the ttl has passed, with controls for the permission off and for a focus before the ttl;
    • an application with no published configuration;
    • the kill switch (enabled: false): no values apply, while the answer's ttl and refresh_on_foreground are still honoured;
    • each scenario waits for a marker that its own answer carries, instead of sleeping; only negative controls wait for an absence;
    • the option off.
  • Mutation checks: removing the restart reconcile or the same-version evaluation each fails its scenario.
  • Under the kill switch the backend sends rum: {} (Resolve returns no values when disabled). Ingest: fc-rum reads _dd.configuration.rc_version from view events of every source, including electron. Rule targeting on sdk is an exact match, and electron is the source name used everywhere else.
  • Acceptance against the dev backend: all seven scenarios (defaults, on-error without and with errors, a published immediate configuration with rc_version stored and applied_version sent back, an emergency stop, an offline restart on the kept configuration, and a native crash in a withheld session) passed with negative controls, verified in the event store and the console; the application's configuration was restored afterwards.
  • docs/ACCEPTANCE.md gains release steps for sampling, sessionOnError and remote configuration, and the SDK's console messages are prefixed [FlashCat electron-sdk].

The main process asks GET /api/v2/rum/config at init and at every new
session (remoteConfigurationEnabled, off by default), keeps the last
accepted answer on disk for the next launch's first draw, and sends its
ETag back as If-None-Match. The ETag of another SDK version is not sent,
so a 304 cannot keep a knob that version did not read.

Sessions read sessionSampleRate and sessionOnError at each draw, and
record the configuration version they were drawn under, reported as
_dd.configuration.rc_version. A configuration published with
activation: immediate ends the running session only where the new
sampling is decisive: a collected session at rate 0, unless it is an
on-error session and the switch stays on; a session drawn at rate 0 when
the rate leaves 0 or the switch turns on. Renderer events take the main
process's version.

The application's custom values are exposed as getRemoteConfig().
The fake intake serves /api/v2/rum/config with a body and ETag, a status,
or a dropped connection, and records what the SDK asked. Each scenario
has a negative control differing only in what it claims.
… upgrades, and revalidate it every ttl

- Keep the activation with the configuration on disk. At startup, before
  anything is collected, a session resumed from before an immediate
  configuration the previous launch kept (an older version, or none) is
  judged by it, so a stop that launch obeyed in memory is not undone by a
  session file it did not get to delete.
- On APP_MAY_EXIT, write an accepted configuration whose asynchronous
  write has not landed, before returning; stop fetching on quit.
- Judge the running session on a same-version answer whose values this
  SDK reads differently (after an upgrade), not only on a higher version.
- Revalidate every ttl the server gives (60 s floor, 600 s default),
  conditionally, with one timer restarted after every request, so a stop
  reaches a long-lived desktop session without waiting for it to turn
  over.
- Treat version 0, what an application with nothing published answers, as
  no version: no rc_version, no applied_version.
- Name the schema version received in the unsupported-schema warning.
…on end to end

The fake config endpoint answers like the real one for an application
with nothing published (200, version 0, switched off). The request
scenario waits for the configuration to be applied before renewing.
…he console allows it, instead of on a timer

Fetching stays at init and at every new session, with no timer between
sessions. When the response sets refresh_on_foreground, a window of the
application gaining focus asks again once what is held is at least ttl
old (60 s floor, 600 s default), conditionally and never alongside a
request in flight. Both are kept in the cache file, so a client that
settled on 304s keeps the permission across restarts.

browser-window-focus is the foreground signal: it fires on every
platform, unlike did-become-active, and means someone is back, unlike
powerMonitor resume. The listeners are removed on stop and on quit.
An emergency stop reaches the running session on focus once the ttl has
passed; no request is made when the console does not allow it, or before
the ttl.
… what it carries safely

- StateFile writes answer whether they landed; the configuration stays
  pending after a failed or superseded write and is written at exit, and
  stopping on quit writes it first, which covers app.exit() and a fetch
  that lands after will-quit.
- At startup a kept immediate configuration always judges the session
  the launch starts with; the decision tree is idempotent, so a session
  drawn under the same version read differently is caught too.
- custom is copied with structuredClone: a key such as __proto__ stays a
  plain key and reaches no prototype.
- A foreground refresh leaves a pending retry to ask and does not re-arm
  the backoff.
- A version 0 answer is no configuration, whatever it carries.
- The unsupported-schema warning and the invalid-option error say what
  stays in effect.
After the awaited write in session creation, resumption and activity,
check that the session is still the active one before scheduling timers
or announcing a renewal, and keep a single four-hour timer, so a session
ended by an immediate configuration mid-write leaves no timer that could
end a later session early.
…er it carries

Each served configuration carries its own marker in custom; the
scenarios wait until the main process holds it instead of sleeping, and
only the negative controls wait for an absence.
…ead a kept version 0 as nothing

- Startup enforcement of a kept immediate configuration skips a session
  drawn under a newer version: the configuration and the session live in
  different files, and the newer configuration's write may be the one
  that was lost. Same, older and missing versions are still judged.
- A version 0 record restored from disk applies no values and no custom,
  as a version 0 answer from the network does.
- The cache identity's exclusion of the client token, and the backend's
  monotonic versions behind the rejection of lower ones, are stated
  where they are relied on; the unsupported-schema warning is shorter.
…at the session is still active

The renewal is announced by session creation itself, right after its
last check, so a session ended by an immediate configuration once saved
is never announced as renewed.
…er version, and bound what is read

- The immediate decision made on a network answer now skips a session
  drawn under a newer version, as startup already did: versions are
  published in order, but a backend node can serve an older snapshot for
  a while, so answers do not arrive in order. The same version is still
  judged.
- An answer is read as a stream and only for a 200; past 256 KiB the
  rest is cancelled and the answer is treated as malformed, keeping the
  last good configuration and retrying. A kept file larger than that is
  not read.
- The pending-retry guard of the foreground refresh is covered.
- The unsupported-schema warning says what stays in force and when to
  upgrade.
…cted 2xx, and re-arm retries after an answer

- Custom values nesting deeper than 16 levels (the backend allows 3)
  make an answer malformed — the last good configuration stays and the
  request is retried — and make a kept file unreadable. The depth is
  measured without recursion, and a serialization failure at exit is
  reported instead of thrown: stopping always aborts the request in
  flight, clears the retry and removes its listeners.
- A 2xx other than 200, such as a 204 from a proxy, is treated like a
  malformed answer and retried.
- The two-retry budget is re-armed by any answer that lands, as well as
  by a new session, so a later outage gets its retries.
A session record is valid with a rate from 0 to 100 and, when present, a
whole non-negative configuration version, so damaged states still fail
closed while records drawn under a remote configuration stay readable.
The incoming session tests pass the sampling as a provider, and the exit
notice in the remote configuration tests carries the terminal flag.
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