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.