远程设置

The remote-settings.sys.mjs 模块提供了获取与 Mozilla 服务器同步的远程设置的功能。

用法

The get() 方法返回特定键的条目列表。每个条目可以具有任意属性,并且只能在服务器上修改。

const { RemoteSettings } = ChromeUtils.import("resource://services-settings/remote-settings.sys.mjs");

const data = await RemoteSettings("a-key").get();

/*
  data == [
    {label: "Yahoo",  enabled: true,  weight: 10, id: "d0782d8d", last_modified: 1522764475905},
    {label: "Google", enabled: true,  weight: 20, id: "8883955f", last_modified: 1521539068414},
    {label: "Ecosia", enabled: false, weight: 5,  id: "337c865d", last_modified: 1520527480321},
  ]
*/

for (const entry of data) {
  // Do something with entry...
  // await InternalAPI.load(entry.id, entry.label, entry.weight);
});

注意

The idlast_modified (时间戳) 属性由服务器分配。

空本地数据库

在新的用户配置文件或最近添加的使用场景中,本地数据库在与服务器同步之前将为空。同步在内部管理,有时会在浏览器启动后几分钟触发。

默认情况下,如果在本地数据库有机会同步之前调用了 .get(),并且没有提供初始数据(见下文),则会从服务器提取设置以避免返回空列表。在这种情况下,第一次调用 .get() 将比后续调用花费更长时间。

可以使用 syncIfEmpty 选项禁用此行为。

重要

如果隐式同步失败(例如网络不可用),则错误会被静默处理,并且 **将返回空列表**。尽管会发送 使用量遥测 状态。

选项

  • filtersorder: 列表可以选择性地进行过滤或排序

    const subset = await RemoteSettings("a-key").get({
      filters: {
        property: "value"
      },
      order: "-weight"
    });
    
  • syncIfEmpty: 如果为 true 且没有本地数据,则查找要加载的打包转储。如果没有,则从网络提取记录。将其设置为 false 以跳过打包转储的加载和网络活动。仅当您的用例能够容忍在第一次同步发生之前出现空列表时才使用此选项(默认值:true)。

    await RemoteSettings("a-key").get({ syncIfEmpty: false });
    
  • verifySignature: 验证本地数据的签名(默认值:false)。如果本地数据被更改,则会抛出错误。这会影响性能,但如果您的用例需要防止本地篡改,则可以使用。

  • emptyListFallback: 如果获取记录失败,则返回空列表(默认值:true)。如果在读取本地数据或同步期间发生错误,则返回空列表。

事件

The on() 函数注册在发生事件时触发的处理程序。

The "sync" 事件允许在服务器端更改远程设置时收到通知。您的处理程序会收到一个包含 data 属性的 event 对象,该属性包含有关更改的信息

  • current: 应用更改后的当前条目列表;

  • createdupdateddeleted: 分别创建/更新/删除的条目列表。

RemoteSettings("a-key").on("sync", event => {
  const { data: { current } } = event;
  for (const entry of current) {
    // Do something with entry...
    // await InternalAPI.reload(entry.id, entry.label, entry.weight);
  }
});

注意

目前,远程设置的同步是通过推送通知以及每 24 小时自己的计时器触发的(请参阅首选项 services.settings.poll_interval)。

文件附件

当条目附带文件时,它具有一个 attachment 属性,其中包含与文件相关的信息(url、哈希值、大小、mime 类型等)。

远程文件不会自动下载。为了使附件保持同步,可以利用提供的辅助程序,如下所示

const client = RemoteSettings("a-key");

client.on("sync", async ({ data: { created, updated, deleted } }) => {
  const toDelete = deleted.filter(d => d.attachment);
  const toDownload = created
    .concat(updated.map(u => u.new))
    .filter(d => d.attachment);

  // Remove local files of deleted records
  await Promise.all(
    toDelete.map(record => client.attachments.deleteDownloaded(record))
  );

  // Download a bundle of all attachments if local cache is empty (see details below)
  client.attachments.cacheAll();

  // OR download new attachments individually
  const fileContents = await Promise.all(
    toDownload.map(async record => {
      const { buffer } = await client.attachments.download(record);
      return buffer;
    });
  );
});
提供的 cacheAll 辅助程序将
  • 如果本地附件缓存不为空,则为无操作 - 使用 pruneAttachments() 清除或将 force 参数设置为 true,如果您知道要覆盖此操作。

  • 如果为集合启用了捆绑,则下载并提取所有附件

  • 返回一个可为空的布尔值以告知调用函数发生了什么 - 如果未尝试下载附件捆绑包(例如:客户端脱机),则为 null - 如果至少有一个附件未能提取,则为 false - 如果找到捆绑包并在没有错误的情况下提取,则为 true

提供的 download 辅助程序将
  • 获取远程二进制内容

  • 将文件写入本地 IndexedDB

  • 检查文件大小

  • 检查内容 SHA256 哈希值

  • 如果附件已存在且本地正常,则不执行任何操作。

重要

以下方面尚未处理(欢迎提供帮助!)

  • 检查可用磁盘空间

  • 保留带宽

  • 恢复大型文件的下载

注意

The download() 方法支持以下选项

  • retries (默认值:3): 网络错误的重试次数

  • fallbackToCache (默认值:false): 允许调用者回退到缓存的文件和记录,如果请求的记录的附件下载失败。这使调用者能够始终拥有有效的附件和记录对,前提是附件至少已检索过一次。

  • fallbackToDump (默认值:false): 在其他加载附件的方法失败时,激活回退到已与客户端打包的转储。有关更多信息,请参阅 services/packaging-attachments

注意

如果不需要在本地写入附件,则还可以使用返回 ArrayBufferdownloadAsBytes() 方法。

还提供了 downloadToDisk()deleteFromDisk() 方法,但通常不建议使用,因为它们容易在配置文件目录中留下多余的文件(请参阅 Bug 1634127)。

初始数据

可以打包服务器记录的转储,在尚未发生同步时将其加载到本地数据库中。

JSON 转储将用作 .get() 的默认数据集,而不是进行往返操作以提取最新数据。它还将减少在第一次同步时需要下载的数据量。

  1. 将服务器记录的 JSON 转储放置在 services/settings/dumps/main/ 文件夹中。使用以下命令

    CID="your-collection"
    curl "https://firefox.settings.services.mozilla.com/v1/buckets/main/collections/${CID}/changeset?_expected=0" | jq '{"data": .changes, "timestamp": .timestamp}' > services/settings/dumps/main/${CID}.json``
    
  2. 将文件名添加到 services/settings/dumps/main/moz.build 中的相关 FINAL_TARGET_FILES 列表中

  • 请考虑使用该集合的应用程序,并且仅在相关的构建中包含转储文件。

  • 如果仅用于 Firefox 桌面版,即 browser/,则将其添加到特定于构建的浏览器部分。

  • 如果用于所有应用程序,即 browser/ 之外或其他特定区域,则将其添加到全局部分。

现在,当从空配置文件调用 RemoteSettings("some-key").get() 时,在返回结果之前将加载 some-key.json 文件。

树中的 JSON 转储会定期由 taskcluster/docker/periodic-updates/scripts/periodic_file_updates.sh 更新。如果您的集合的树内转储不应由此自动化更新,请将 JSON 文件放在 services/settings/static-dumps/ 中。

注意

上面的示例使用“main”,因为这是默认的存储桶名称。如果您自定义了存储桶名称,请使用实际的存储桶名称代替“main”。

打包附件

默认情况下,附件不包含在 JSON 转储中。您可以选择将附件与客户端打包,例如,如果在第一次启动时需要数据可用并且不需要网络活动,或者大多数用户无论如何都会下载附件。仅在需要时才打包附件,因为它们会增加 Firefox 安装程序的文件大小。

要为 download() 方法的使用者打包附件

  1. 从服务器记录的 JSON 转储中选择所需的附件记录,并将其放置在 services/settings/dumps/<bucket name>/<collection name>/<attachment id>.meta.json 中。The <attachment id> 默认设置为记录的 id 字段。如果此 id 不是固定的,则必须选择一个可以作为长期附件标识符依赖的自定义 ID。有关更多详细信息,请参阅下面的说明。

  2. 下载与记录关联的附件,并将其放置在 services/settings/dumps/<bucket name>/<collection name>/<attachment id> 中。

  3. 更新 taskcluster/docker/periodic-updates/scripts/periodic_file_updates.sh 并添加附件,方法是编辑 compare_remote_settings_files 函数并描述附件。与 JSON 转储不同,附件必须在更新脚本中明确列出,因为附件选择逻辑需要在脚本中的 jq 过滤器中进行编码。有关示例,请参阅 Bug 1636158

  4. 在集合文件夹的moz.build文件中注册<attachment id>.meta.json<attachment id>的位置,以及可能存在的package-manifest.in文件,具体方法如前面关于注册JSON转储 <services/initial-data>的部分所述。

注意

<attachment id>用于推导出打包附件转储的文件名,并用作缓存的键,网络上的附件更新将保存在该缓存中。附件标识符预计在客户端应用程序更新期间保持不变。如果无法满足此预期,则应使用附件下载器的download方法的attachmentId选项将附件ID覆盖为自定义(稳定)值。为了跟踪缓存的附件并防止其被自动清理,将必须在RemoteSettings客户端构造函数的keepAttachmentsIds = [<attachment id>]选项中显式列出附件标识符。

注意

.meta.json文件的内容已包含在记录中,但与主要记录集分开,以确保原始记录及其数据在打包或下载的记录独立于其存在的情况下可用。此文件在未来的更新中可能会变为可选,请参阅Bug 1640059

同步流程

同步流程包括提取最近的更改、将它们与本地数据合并以及验证结果的完整性。

../../_images/synchronization-flow.svg

重要

如上所示,我们可能会遇到同步失败并使本地数据库处于空状态的情况。

目标和A/B测试

为了将设置交付给特定人群子集,您可以在服务器上编辑记录时为条目设置目标(平台、语言、渠道、版本范围、首选项值、样本等)。

从客户端API的角度来看,这是完全透明的:.get()方法(以及事件数据)将始终过滤目标匹配的条目。

注意

远程设置目标遵循与Normandy配方客户端相同的方法(即 JEXL 过滤器表达式)。

采用率遥测

为了监控远程设置的传播方式,会收集一些采用率遥测数据。

它被提交到一个唯一的键控直方图,其ID为UPTAKE_REMOTE_CONTENT_RESULT_1,键以main/为前缀(例如,在上面的示例中为main/a-key)。

创建新的远程设置

工作人员可以根据此文档创建新的远程设置类型。

它主要包括以下步骤:

  1. 选择一个键(例如search-providers

  2. 为编辑器和审阅者组分配协作者

  3. 可选)定义JSONSchema以验证条目

  4. 可选)允许条目附加附件

完成后

  1. 创建、修改或删除条目,并让审阅者批准更改

  2. 等待Firefox获取您设置键的更改

全局通知

更改轮询过程会发送两个通知,观察者可以注册到这些通知:

  • remote-settings:changes-poll-start:正在开始轮询更改。由计划的计时器或推送广播触发。

  • remote-settings:changes-poll-end:更改轮询已结束。

  • remote-settings:sync-error:同步发生错误。通知主题在wrappedJSObject属性中提供有关错误和受影响集合的信息。

  • remote-settings:broken-sync-error:同步似乎持续失败。配置文件有风险。

const observer = {
  observe(aSubject, aTopic, aData) {
    Services.obs.removeObserver(this, "remote-settings:changes-poll-start");

    const { expectedTimestamp } = JSON.parse(aData);
    console.log("Polling started", expectedTimestamp ? "from push broadcast" : "by scheduled trigger");
  },
};
Services.obs.addObserver(observer, "remote-settings:changes-poll-start");

高级选项

localFields:保留为本地的记录字段

在同步期间,本地数据库与服务器数据进行比较。任何差异都将被远程版本覆盖。

在某些用例中,需要使用记录上的额外属性存储一些状态。localFields选项允许指定在同步期间应保留哪些记录字段名称。

const client = RemoteSettings("a-collection", {
  localFields: [ "userNotified", "userResponse" ],
});

filterFunc:自定义过滤函数

默认情况下,.get()返回的条目会根据filter_expression字段中的JEXL表达式结果进行过滤。filterFunc选项允许执行自定义过滤器(异步)函数,如果保留则应返回记录(已修改或未修改),如果过滤掉则应返回虚假值。

const client = RemoteSettings("a-collection", {
  filterFunc: (record, environment) => {
    const { enabled, ...entry } = record;
    return enabled ? entry : null;
  }
});

调试和手动测试

日志记录

要启用详细日志记录,请将日志级别首选项设置为debug

Services.prefs.setStringPref("services.settings.loglevel", "debug");

远程设置开发工具

远程设置开发工具扩展提供了一些工具来检查同步状态,更改远程服务器或切换到预览模式以签核挂起的更改。有关专用存储库的更多信息

预览模式

启用预览模式以预览将在服务器上进行审查的更改。这可以通过远程设置开发工具或以编程方式实现:

RemoteSettings.enablePreviewMode(true);

为了在**启动时**提取预览数据或在重新启动后保持预览数据,请在配置文件首选项(即user.js)中将services.settings.preview_enabled设置为true。出于安全原因,在发布版和ESR版中,您需要使用MOZ_REMOTE_SETTINGS_DEVTOOLS=1环境变量运行应用程序才能考虑该首选项。请注意,切换首选项在重新启动之前没有任何效果。

手动触发同步

可以使用pollChanges()手动触发所有已知远程设置客户端的同步。

await RemoteSettings.pollChanges()

为了在轮询更改期间忽略上次同步状态,请设置full选项。

await RemoteSettings.pollChanges({ full: true })

可以使用.sync()方法强制单个客户端同步。

await RemoteSettings("a-key").sync();

重要

以上方法仅在开发或调试期间相关,不应在生产代码中调用。

检查本地数据

可以通过浏览器工具箱中的存储检查器访问远程设置的内部IndexedDB。浏览器工具箱

例如,可以在remote-settings数据库中,通过浏览器工具箱 > 存储 > IndexedDB > chrome,在records存储中访问"key"集合的本地数据。

删除所有本地数据

可以使用以下方法删除所有本地数据(**每个集合**的数据),包括下载的附件:

await RemoteSettings.clearAll();

单元测试

作为前言,我们想强调一点,您的测试不应测试远程设置本身。您的测试应该假设远程设置有效,并且应该只对集成部分运行断言。例如,如果您发现自己正在模拟服务器响应,那么您的测试可能会超出其职责范围。

如果您的代码依赖于"sync"事件,您可能会对伪造此事件并确保您的代码按预期运行感兴趣。如果它依赖于.get(),您可能希望插入一些伪造的本地数据。

模拟"sync"事件

您可以伪造一个payload,其中包含如上所述的事件属性,并将其发出 :)

const payload = {
  current: [{ id: "abc", age: 43 }],
  created: [],
  updated: [{ old: { id: "abc", age: 42 }, new: { id: "abc", age: 43 }}],
  deleted: [],
};

await RemoteSettings("a-key").emit("sync", { data: payload });

操作本地数据

可以通过.db属性获取底层数据库的句柄。

const db = RemoteSettings("a-key").db;

并且可以手动创建记录(就像它们是从服务器同步的一样)。

const record = await db.create({
  id: "a-custom-string-or-uuid",
  domain: "website.com",
  usernameSelector: "#login-account",
  passwordSelector: "#pass-signin",
});

如果没有设置时间戳,任何对.get()的调用都将触发初始数据(JSON转储)的加载(如果有),或者将触发同步。为避免这种情况,请存储一个伪造的时间戳。我们使用Date.now()而不是任意数字,以确保它高于转储的时间戳,从而防止从测试中加载转储。

await db.importChanges({}, Date.now());

为了绕过RemoteSettings("key").get()的潜在目标过滤,可以使用collection.list()获取记录的底层列表。

const { data: subset } = await db.list({
  filters: {
    "property": "value"
  }
});

可以使用clear()刷新本地数据。

await db.clear()

其他

我们在https://remote-settings.readthedocs.io/上提供了更多文档,内容涉及如何在本地运行服务器、管理附件或使用REST API等。

关于黑名单

安全设置以及加载项、插件和GFX黑名单是远程设置的第一个用例,因此具有一些特殊性。

例如,它们利用高级自定义选项(存储桶、内容签名证书、目标过滤等)。为了获取对这些客户端的引用,必须首先执行其初始化代码。

const {RemoteSecuritySettings} =
  ChromeUtils.importESModule("resource://gre/modules/psm/RemoteSecuritySettings.sys.mjs");

RemoteSecuritySettings.init();


const {BlocklistPrivate} =
  ChromeUtils.importESModule("resource://gre/modules/Blocklist.sys.mjs");

BlocklistPrivate.ExtensionBlocklistRS._ensureInitialized();
BlocklistPrivate.PluginBlocklistRS._ensureInitialized();
BlocklistPrivate.GfxBlocklistRS._ensureInitialized();

然后,为了访问特定的客户端实例,必须指定bucketName

const client = RemoteSettings("onecrl", { bucketName: "security-state" });

在存储检查器中,IndexedDB内部存储将以security-state而不是main为前缀(例如security-state/onecrl)。