feat(remote-config): expose the v2 snapshot API - #865
Closed
shameondev wants to merge 3 commits into
Closed
Conversation
Adds the customer-facing Remote Config v2 surface on top of the internal
snapshot core, fetch coordinator and gateway transport landed in the
previous slices. Nothing here re-implements them: the public types are
thin, immutable adapters over the resolution ladder, the read guard and
the coordinator's waiter model.
The surface (all marked @ExperimentalQonversionApi, so it is not yet a
stability promise):
- Qonversion.remoteConfigSnapshots() -> QRemoteConfigSnapshots, with
fetch(timeoutMs?), activate(), fetchAndActivate(), an immutable
`current` snapshot, a synchronous bundled-fallback getter and
subscribeOnConfigUpdate().
- Reads return {value, source}: raw JSON, an opaque JSON tree, or a
caller-decoded type. The decoder is the per-key validator seam, so a
rejected value falls to the previously activated release (cache) and
then to the bundled defaults.
- fetch's timeout bounds the wait, not the request: the completion
reports the best available snapshot while the request keeps running and
is still admitted when it lands.
- Activation stays a whole-release atomic swap; an immediate-policy
release performs that same swap on admission and notifies subscribers
with the changed-key diff and per-key metadata.
- Identity changes switch the scope synchronously (the previous
identity's release is never readable afterwards) and force a fetch; an
identify that only attaches an external id re-reads targeting without
dropping the served release.
The pipeline is dormant unless the app passes a QRemoteConfigV2Config:
without it no store, thread, HTTP client or base URL is constructed, and
every fetch completes with NotConfigured. There is no default endpoint.
Tests cover the contract end to end against a real MockWebServer with the
real core, guard and coordinator: timeout semantics, change detection,
all three ladder positions, raw vs typed reads, the pre-activate fallback
getter, subscription diffs, immediate auto-activation, the identity
switch (old snapshot excluded, per-identity session, install date pinned
against a real PackageManager) and main-thread delivery.
Claude-Session: https://claude.ai/code/session_018pHXqfbxkMQJFzUZ3jW4A8
QRemoteConfigV2Config demanded a `contextFingerprint`: the app had to hand the SDK the fingerprint the gateway resolves for it, and an admitted snapshot had to carry exactly that value. Nobody could supply it correctly, because it is not an app-level constant at all. configurator computes it in BuildResolvedSnapshotContextFingerprint (internal/domain/remoteconfigv2/resolved_snapshot.go) by hashing the canonical user uid, randomization id, platform, country, app version, OS version, SDK version, locale, device model, media source / campaign, install and created timestamps, purchases, active experiment uids and custom user properties. It is a per-response tag over mutable targeting inputs, not an identity binding: it rotates on any app or OS update, a language switch, a purchase, a property edit or an experiment enrollment. So the fingerprint is treated as what it is: - it is gone from the public QRemoteConfigV2Config and from the internal RemoteConfigSnapshotEnvelopeExpectation — nothing configures it and nothing compares it against a previous response; - the parser keeps validating its *shape* (64 lowercase hex, required member) and carries it through as an opaque per-response tag; a value that changes between two admissions in the same scope is normal and admitted; - it is still stored with the release, as informational data for logs and bug reports, and it still participates in the release content digest; - the KDoc on the public config, the expectation and the release all state the rule verbatim, so the "pin it across fetches" idea does not get re-invented: pinning it would freeze an identity's config until logout the first time the user updated the app or changed their language. Identity isolation is unchanged and stays where it already lives: each snapshot read travels on a session token minted for exactly one identity, the gateway routes on that session, and the snapshot / session / fetch-policy stores address each identity through its own salted scope digest. No compatibility shim: the surface is @ExperimentalQonversionApi and has never shipped as stable, so the constructor parameter is simply removed. Tests: a rotated fingerprint is admitted end to end through the public API over MockWebServer (and at the core, where the previously admitted release keeps its own tag), every malformed or missing fingerprint is still refused by the parser, and the project / environment admission boundaries are unchanged. Claude-Session: https://claude.ai/code/session_018pHXqfbxkMQJFzUZ3jW4A8
The numeric project id a v2 snapshot is admitted against was supplied by the app through QRemoteConfigV2Config, but the app is not its source: the SDK is told it by the gateway's session bootstrap. Asking for it added a public value that could only ever be typed wrong. BREAKING (experimental surface): QRemoteConfigV2Config no longer takes projectId. Callers drop the argument; nothing else changes for them. The id is now learned and pinned per project key + environment by RemoteConfigProjectIdRegistry, durably, next to the session state: the first bootstrap establishes it, every later session must agree, and one that does not is refused as the typed RemoteConfigFetchResponse .ProjectMismatch before a snapshot is ever read — never re-learned. The in-memory pin is authoritative for the process, so a storage failure cannot downgrade a conflict into a silent re-learn. A malformed id is reported apart from a conflict and stays an ordinary failure. Because the pin is established mid-fetch, the envelope expectation moved from the admission claim to the admission itself: beginAdmission takes only the scope, admitCandidate takes the project id the response was served for, and the environment is read from the admitting scope rather than restated. RemoteConfigFetchBinding was exactly a scope plus that expectation, so it is gone and the coordinator binds to the scope. A mismatch feeds the failure backoff. It is permanent until the gateway is fixed and costs a bootstrap round trip each time, and forced fetches bypass the minimum interval but not the backoff gate, so an identify/logout loop cannot turn a misrouted gateway into a request storm. The check this buys is server-vs-server consistency plus trust on first bootstrap, not proof that a snapshot belongs to the project the developer meant to target; QRemoteConfigV2Config's KDoc says so.
Contributor
Author
|
Консолидировано в release-train PR (один PR на репозиторий). Ветка сохранена. |
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.
What
Stacked on #864. The client-facing contract as an opt-in experimental surface (
@ExperimentalQonversionApi— new@RequiresOptInmarker; dormant unlesssetRemoteConfigV2Configis supplied; no default-on, no prod constants):Qonversion.remoteConfigSnapshots()facade:fetch(timeoutMs?),activate,fetchAndActivate, immutablecurrent,fallbackRemoteConfigValue,subscribeOnConfigUpdateQRemoteConfigValue{value, source(Server|Cache|Fallback), variationUid, applyPolicy, metadataJson};rawValue/jsonValue/typedvalue(decoder)identify()that keeps the uid still refreshes targeting via a new bridge; device install date survives logoutVerification
./gradlew :sdk:test detektAll: BUILD SUCCESSFUL, 429 tests (agent runs ×3 incl. --rerun-tasks + independent re-run by the orchestrating session);assembleReleaseOKidentify()-same-uid never refreshing v2, "null" metadata string, TimedOut snapshot semantics, distinguishable refused-snapshot logging, plus a latent ordering flake in the pre-existing coordinator testCross-repo follow-up (both SDK slices hit this independently)
context_fingerprintis computed by configurator with a server secret, so the SDK cannot derive it — it is temporarily an explicitQRemoteConfigV2Configparameter. The gateway session-bootstrap response should return it; then the parameter disappears. Until then cold-start integration requires the hand-off — consistent with the surface being dormant.