File: remote-settings.md

package info (click to toggle)
firefox 147.0.3-1
  • links: PTS, VCS
  • area: main
  • in suites: sid
  • size: 4,683,320 kB
  • sloc: cpp: 7,607,359; javascript: 6,533,295; ansic: 3,775,223; python: 1,415,500; xml: 634,561; asm: 438,949; java: 186,241; sh: 62,752; makefile: 18,079; objc: 13,092; perl: 12,808; yacc: 4,583; cs: 3,846; pascal: 3,448; lex: 1,720; ruby: 1,003; php: 436; lisp: 258; awk: 247; sql: 66; sed: 54; csh: 10; exp: 6
file content (265 lines) | stat: -rw-r--r-- 9,746 bytes parent folder | download | duplicates (12)
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.