Remote Settings Client¶
The API for the Remote Settings can be found in the Mozilla Rust components Kotlin API Reference and Swift API Reference.
Prerequisites¶
That 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¶
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
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.
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)
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.
val remoteSettingsClient = appWideRemoteSettingsService.makeClient("my-collection")
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.
fun processRecords(remoteSettingsClient: RemoteSettingsClient) {
val records = remoteSettingsClient.getRecords()
if (records != null) {
for (record in records) {
processRecord(record.id, deserialize(record.field))
}
}
}
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.
fun valueOfFeature(remoteSettingsClient: RemoteSettingsClient): Boolean {
val records = remoteSettingsClient.getRecordsMap()
if (records != null) {
return deserializeFeatureValue(records["featureName"].field)
} else {
return DEFAULT_FEATURE_VALUE
}
}
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.
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
}
}
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:
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}")
}
}
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.