Skip to content

Read the console's sampling settings on HarmonyOS - #33

Open
Fiona2016 wants to merge 7 commits into
mainfrom
feat/rum-remote-config
Open

Read the console's sampling settings on HarmonyOS#33
Fiona2016 wants to merge 7 commits into
mainfrom
feat/rum-remote-config

Conversation

@Fiona2016

@Fiona2016 Fiona2016 commented Aug 24, 2026

Copy link
Copy Markdown
Collaborator

HarmonyOS applications currently ship their RUM sampling rate with the app. This adds opt-in remote configuration so the console can change sampling and application-defined custom values without an app release.

Behavior

  • setRemoteConfigurationEnabled(true) fetches at startup and each new session. Foreground refresh is optional and respects the server TTL. Rates, custom values, version, ETag, and refresh settings survive process restarts. A startup 304 preserves the foreground refresh policy; incomplete cached refresh settings trigger a full response without discarding the cached rate.
  • Invalid response envelopes, unsupported schemas, and stale versions leave existing configuration unchanged. Invalid envelopes cannot replace the cached version and block later valid updates. Requests never delay initialization, and failures have a bounded, jittered retry budget.
  • Rates normally apply to the next session. Effective changes through zero and supported immediate activation can end the current session. A beforeSampling hook has the last word, including when only custom allow-list values change. Deferred changes and repeated effective settings do not cause extra draws. Forced sessions remain collected.
  • View events carry the session's actual draw rate and configuration version. Forced sessions report 100% without a configuration version.

Application API

FlashcatRum.enable(new RumConfigurationBuilder(applicationId)
  .setSessionSampleRate(100)
  .setRemoteConfigurationEnabled(true)
  .setBeforeSampling((context: BeforeSamplingContext): number | undefined => {
    return isBeingDebugged(context.custom) ? 100 : undefined;
  })
  .build());

GlobalRumMonitor.get().getRemoteConfig();
GlobalRumMonitor.get().setForcedSession();

The hook must be synchronous, deterministic, and free of side effects: it is called at session creation and when configuration arrives. Exceptions and invalid rates leave the incoming rate unchanged. Remote configuration is disabled by default.

The core supplies intake credentials and persistent settings because feature modules do not receive a HarmonyOS Context. The controller owns fetching and storage; the RUM application scope decides whether a change ends a session. Draw metadata stays in memory for the session lifetime.

Validation

  • 205 unit tests pass, including 57 remote-configuration tests covering parsing, persistence, retries, 304, foreground refresh, session activation, hooks, and forced sessions.
  • Local HAR, demo HAP, and UnitTestBuild gates pass.
  • Eight emulator checks against a local HTTP endpoint pass: initial 200, restart with 304, foreground refresh after restart, custom-only allow-list addition and removal, malformed high-version response rejection, recovery with a valid version, and disabled foreground refresh as a negative control.

This enables SDK support for activation: immediate and refresh_on_foreground; console policy and SDK publication are separate from this change.

RUM on HarmonyOS collected at whatever rate the app was built with: moving
that rate meant shipping a release and waiting for it to roll out, while the
other platforms could already turn the knob from the console.

The settings are read from the same endpoint the other SDKs use, at the rhythm
of the sessions that consume them: once at start-up, once per new session, and
on return to the foreground when the console asks for it. What arrives is
stored, so the first session of a launch is already drawn under it. Nothing
here can hold up initialisation or interrupt collection — a request that fails,
times out or comes back unreadable leaves the stored values exactly as they
were, because wiping them on a bad minute would swing a fleet back to the
values it was built with, the opposite of what someone who moved a knob wants.

A published rate applies to the NEXT session drawn; a session already under way
is never re-decided. View documents now report the rate the session was
actually drawn with plus the settings version, so extrapolation and audits line
up with the draw that kept the session rather than with the init values.

Two application-facing controls come with it:

- setForcedSession() keeps a visitor the rate would have dropped — the support
  case where someone needs this one user's sessions whatever the fleet is on.
- beforeSampling() has the last word on the rate, called synchronously at each
  draw with the rate that would apply and the console's custom values. A throw
  or an out-of-range answer is ignored: a mistake in the host application must
  not take collection down with it.

Feature modules get no HarmonyOS Context of their own, so the credentials and
the small amount of storage this needs come from the core, which already owns
both. The demo app gains a scenario that drives the whole path from the command
line.
Three things the first pass left behind: `stopSession`'s documentation ended up
attached to a method inserted above it, two error-to-string helpers with one
caller each where every other call site in this codebase writes the expression
inline, and the same JSON-object parse in both the store and the controller —
the store's is now shared, since the endpoint's body and the store's own
entries need exactly the same tolerance.
…ot read

The server states the shape of the body it wrote in `schema_version`. A
reader that guesses instead of checking is exactly what that field
exists to prevent: a payload shaped for a newer reader can be misread
field by field while every individual field still parses, and
half-understood sampling settings are worse than none.

The check has to be honoured by the first SDK that ships. Refusing can
only be done by code already on the device, so a version added later
would be ignored by exactly the clients it needs to protect.

`apply()` now reports one of three outcomes instead of a boolean:

  APPLIED             the body was read and its values are stored
  UNREADABLE          not a configuration at all - ask again
  UNSUPPORTED_SCHEMA  a contract this SDK does not read - refused whole

Only UNREADABLE is retried. A schema we do not know is an answer, not a
failure: asking again would fetch the same refusal, so a server-side
schema bump cannot turn a fleet into a retry storm. Nothing from a
refused body reaches storage, not even the fields that happened to
parse.
A response with no schema stamp at all was refused as a shape this SDK cannot
read. A body without one is, by construction, the shape that existed before the
stamp did — which is the shape this reader was written against. Refusing it
switches remote configuration silently off against a server that merely
predates the field, and nothing says so: the refusal path is the same one a
body we genuinely cannot read takes, so there is no error to notice.

Only a stamp that is present and unrecognised is a refusal now, which is what
the web SDK already did. The two no longer disagree about the same response.
forceSession() returned early when the session was already forced and being
collected, then set the flag and returned again on the same "already being
collected" condition. Reaching the first guard meant the flag was already set,
so the only thing it did was skip setting it to the value it already had.
Four things about the remote-configuration path were wrong, and they share a
theme: what a session reports, and who gets to end one.

A forced session reported the rate the console published and the version that
went with it, though neither decided it — the draw was skipped precisely
because the application asked for this visitor whatever the fleet is on. The
backend weights a session by 100/rate, so one visitor kept by
`setForcedSession()` under a published 3% arrived as thirty-three real ones,
credited to a version it ignored. A forced session now reports 100 and no
version.

View documents carried no configuration block at all unless the application had
opted into remote configuration. But the rate is how the backend weights every
session, and one that reports nothing is counted once — the same as one from an
SDK too old to say so. Every session now reports the rate it was drawn with;
the version is still reported only when a published configuration decided it.

A response older than the one already in force replaced the stored values and
their validator, so the client would draw under superseded settings and then
revalidate against them. Rollbacks are published as a new version, so an older
body can only come from a replica that has not caught up or an intermediary
answering with something it held. It is now refused — and refused before any
other field is read, so its ttl and its foreground-refresh permission cannot
outlive the values it was refused for.

A rate crossing zero never reached the session already running. Switching
collection on left everyone already in the app dark until their sessions
expired — up to four hours, and nothing at all is indistinguishable from
broken — while switching it off was not the emergency stop it looks like. That
decision now lives in `RumApplicationScope`, the only part that knows whether
the running session is being collected, whether the application forced it, and
what `beforeSampling` says about the rate that would apply next:

- a session that could never have been collected is replaced only when it was
  drawn at zero and the next rate can collect something. One that lost a fair
  draw is left alone; re-drawing it on every publish would bias collection
  upward.
- a session being collected ends only when the next rate is zero, or when the
  console asked for `immediate`.
- a session the application forced, or that its own hook keeps, is never ended
  by a rate. Every replacement would be kept too, so ending one costs the view
  the visitor is on and buys nothing.

The controller now announces what changed and leaves the judgement there.

Its retry schedule becomes a constructor parameter, defaulted to the real one,
for the same reason the fetcher is one: the backoff, the unchanged-answer path
and the foreground rhythm are the half of this feature that must not be got
wrong, and a test that had to wait out a real minute-long delay would never be
written. Twenty-four tests cover the paths that had none — the retry budget
and its re-arming, an unchanged answer not leaving the controller wedged, a
response landing after the SDK stopped, the server's ttl, a refused body
leaving the rhythm alone, the seam that asks the console again on every new
session, and a published change travelling from the response all the way to
the session it decides.
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