1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265
|
# Remote Settings Client
The API for the Remote Settings can be found in the Mozilla Rust components [Kotlin API Reference](https://mozilla.github.io/application-services/kotlin/kotlin-components-docs/mozilla.appservices.remotesettings/index.html) and [Swift API Reference](https://mozilla.github.io/application-services/swift/Classes/RemoteSettings.html).
## Prerequisites
That {doc}`viaduct` must be initialized during application startup.
## Async
The Remote Settings API is synchronous, which means calling it directly will block the current
thread. To deal with this, all current consumers wrap the API in order to make it async. For
details on this wrapping, see the consumer code itself.
## Importing items
:::{tab-set-code}
```kotlin
import mozilla.appservices.remotesettings.RemoteSettingsClient
import mozilla.appservices.remotesettings.RemoteSettingsConfig2
import mozilla.appservices.remotesettings.RemoteSettingsException
import mozilla.appservices.remotesettings.RemoteSettingsServer
import mozilla.appservices.remotesettings.RemoteSettingsService
```
```swift
import MozillaAppServices
```
:::
## Application-level setup
Applications should create an app-wide `RemoteSettingsService`. This manages which
remote settings server to make requests to, syncing data with that server, etc.
`RemoteSettingsService` instances are created using a config. The name is because there is an
older/semi-deprecated `RemoteSettingsConfig` class. The DISCO team plans to rename this class and
remove the `2` once we move consumers over to the new API.
:::{tab-set-code}
```kotlin
val config = RemoteSettingsConfig2(
// Remote Settings server to connect to. Other options are:
// * RemoteSettingsServer.Stage()
// * RemoteSettingsServer.Dev()
// * RemoteSettingsServer.Custom(url)
server = RemoteSettingsServer.Prod(),
storageDir = pathToDir,
// optional field to fetch from a non-standard bucket
bucketName = if (usePreviewBucket) { "main-preview" } else { "main" }
)
val appWideRemoteSettingsService = RemoteSettingsService(config)
```
```swift
let config = RemoteSettingsConfig2(
// Remote Settings server to connect to. Other options are:
// * RemoteSettingsServer.stage
// * RemoteSettingsServer.dev
// * RemoteSettingsServer.custom(url: url)
server = RemoteSettingsServer.prod,
storageDir = pathToDir,
// optional field to fetch from a non-standard bucket
bucketName = if usePreviewBucket { "main-preview" } else { "main" }
)
let appWideRemoteSettingsService = RemoteSettingsService(config: config)
```
:::
## Creating Remote Settings clients
`RemoteSettingsService` instances can be used to create new `RemoteSettingsClient` instances that
fetch remote settings records for a particular collection.
:::{tab-set-code}
```kotlin
val remoteSettingsClient = appWideRemoteSettingsService.makeClient("my-collection")
```
```swift
let remoteSettingsClient = appWideRemoteSettingsService.makeClient(collection: "my-collection")
```
:::
## Getting records
`RemoteSettingsClient` instances can be used to fetch remote settings records. Records have some standard attributes
(`id`, `lastModified`, etc) and also have the `fields` attribute which stores all other JSON data
serialized as a string.
`getRecords` does not make a network request, instead it returns the last synced data with the
server. This makes it safe to call in early startup where starting up a new network request is not
desirable. However, this means that it returns a nullable value, which must be checked.
:::{tab-set-code}
```kotlin
fun processRecords(remoteSettingsClient: RemoteSettingsClient) {
val records = remoteSettingsClient.getRecords()
if (records != null) {
for (record in records) {
processRecord(record.id, deserialize(record.field))
}
}
}
```
```swift
func processRecords(remoteSettingsClient: RemoteSettingsClient) {
let records = remoteSettingsClient.getRecords()
if let records = records {
for record in records {
processRecord(id: record.id, recordData: deserialize(record.field))
}
}
}
```
:::
`getRecordsMap` works similarly, but it returns a map with the record ID as the key. Again,
this value is nullable since the client may not have synced any data yet.
:::{tab-set-code}
```kotlin
fun valueOfFeature(remoteSettingsClient: RemoteSettingsClient): Boolean {
val records = remoteSettingsClient.getRecordsMap()
if (records != null) {
return deserializeFeatureValue(records["featureName"].field)
} else {
return DEFAULT_FEATURE_VALUE
}
}
```
```swift
func valueOfFeature(remoteSettingsClient: RemoteSettingsClient): Bool {
let records = remoteSettingsClient.getRecordsMap()
if let records = records {
return deserializeFeatureValue(recordData: records["featureName"].field)
} else {
return DEFAULT_FEATURE_VALUE
}
}
```
:::
Both `getRecords` and `getRecordsMap` input an optional `syncIfEmpty` parameter. Pass
`syncIfEmpty=true` to sync records with the server if they haven't been synced before. Even with
this parameter, you should still check for null, which will be returned if the network request
fails. `syncIfEmpty` should be used with caution, since there can be a delay in fetching the
setting. For example, it could delay UI updates.
## Getting attachment data
`RemoteSettingsRecord` instances have an optional attachment field. If present, you can download
the attachment data as a byte array using `RemoteSettingsClient.getAttachment`. This will make a
network request unless the attachment data is cached.
:::{tab-set-code}
```kotlin
val records = remoteSettingsClient.getRecords()
if (records.size > 0 && records[0].attachment != null) {
val attachmentData: ByteArray = remoteSettingsClient.getAttachment(records[0].attachment.location)
// do something with the attachment data
}
}
```
```swift
let records = remoteSettingsClient.getRecords()
if (records.count > 0 && records[0].attachment != nil) {
val attachmentData: Data = remoteSettingsClient.getAttachment(location: records[0].attachment.location)
// do something with the attachment data
}
```
:::
## Syncing with the server
Use `RemoteSettingsService.sync()` to synchronize remote settings data with the server. This will
fetch remote settings data for all clients created with the `RemoteSettingsService` that are still
alive. This synchronization can take a significant amount of time and should probably be run in a
worker queue.
## Exception handling
The Remote Settings component defines the following error hierarchy:
- **RemoteSettingsError**: Base error
- **RemoteSettingsError.Network(reason: string)**: Network error while making a request
- **RemoteSettingsError.Backoff(seconds: int)**: The server requested a request backoff of at least [seconds]
- **RemoteSettingsError.Other(reason: string)**: Catch-all for other remote settings errors
How this works depends on the language:
:::{tab-set-code}
```kotlin
fun remoteSettingsPeriodicSync() {
// On Kotlin, errors are sealed/nested classes.
// "Error" is replaced with "Exception" for consistency with other exceptions.
try {
appWideRemoteSettingsService.sync()
} catch (e: RemoteSettingsException.Network) {
// Try again after 5 minutes
Log.w("Network error when syncing Remote Settings: ${e.reason}")
scheduleRemoteSettingsSyncAfter(300)
} catch (e: RemoteSettingsException.Backoff) {
Log.w("Backoff error when syncing Remote Settings")
scheduleRemoteSettingsSyncAfter(e.seconds)
} catch (e: RemoteSettingsException.Other) {
// There's no reason to think another sync will work.
// Sync again using the normal schedule
Log.w("Unexpected error when syncing Remote Settings: ${e.reason}")
}
}
```
```swift
func remoteSettingsPeriodicSync() {
// On Swift errors are members of the base error enum.
do {
appWideRemoteSettingsService.sync()
} catch RemoteSettingsError.Network(let reason) {
// Try again after 5 minutes
print("Network error when syncing Remote Settings: \(reason)")
scheduleRemoteSettingsSyncAfter(seconds: 300)
} catch RemoteSettingsError.Backoff(let seconds) {
print("Backoff error when syncing Remote Settings")
scheduleRemoteSettingsSyncAfter(seconds: seconds)
} catch RemoteSettingsError.Other(let reason) {
// There's no reason to think another sync will work.
// Sync again using the normal schedule
print("Unexpected error when syncing Remote Settings: \(reason)")
}
}
```
:::
`RemoteSettingsClient.getRecords` and `RemoteSettingsClient.getRecordsMap` never throw. If they
encounter an error, they will record it using internal metrics/error reports then return `null`.
The reason for this is that code that calls those methods will certainly handle exceptions in the
same way as `null` and this avoids duplicating that code.
## Preventing nulls with scheduled downloads
The Remote Settings module has a system in place where we download Remote Settings collections on a
regular basis and store the data inside the library itself. This data is used as a fallback
whenever `getRecords` or `getRecordsMap` would return `null`. This can simplified consumer, since
it doesn't need an extra branch to handle missing data. This also can reduce network traffic, since
we only need to fetch new records if they've been updated since the last download.
If you would like your collection to be downloaded on this schedule, please contact the DISCO team
and we can set it up.
|