跳到正文
AppFoyer
免费开始

指南 上架之后

让你的隐私政策与构建保持同步

隐私政策只写一次,而某个拉取请求加入 SDK 的那天它就过时了。如何在拉取请求本身上发现这一点:一个只读取构建文件的免费检查。

记录你的页面是根据哪些 SDK、数据类型和权限写成的,然后把每个拉取请求的构建文件与这份记录进行比对。当出现记录中没有的依赖或权限时,拉取请求会在发布之前就把它显示出来——此时隐私政策和商店表单都还很容易更新。

几分钟就能搞定

AppFoyer 托管本指南所讲的页面——隐私政策、服务条款、支持、账号删除和 app-ads.txt——放在它们自己的子域名上,你无需自己的域名或服务器。一个应用免费,而且商店要求的任何内容都绝不放在付费墙后面。

核查于 2026年9月30日。 商店政策会变,有时不会事先通知。下文所有内容都链接到官方文档,以那个页面为准,而不是本页。如果两者不一致,以商店为准。 本指南译自英文;引用的商店文档保留英文原文。

写隐私政策是一次性的工作。让它保持真实却不是:政策描述的是你写它时的那个构建,而构建一直在变。某个拉取请求加入了崩溃上报、订阅库或位置权限,App 发布了,而隐私政策、Data safety 表单和 App Privacy 详情描述的仍然是旧的 App。

两个商店都把这视为你的问题,而不是它们的。Google Play 表示,你的 Data safety 答案必须始终保持准确和完整,而且 App 中第三方库和 SDK 收集的数据要由你来声明。Apple 对 App Privacy 详情也有同样的说法,并补充说你可以随时在 App Store Connect 中修改答案,无需提交更新。

偏差实际上从哪里来

几乎从来不是因为一个”收集更多数据”的决定。它来自日常的工程工作:

  • 新的依赖。 崩溃上报、分析、付费墙 SDK、地图库。
  • 新的权限。 为某个功能使用位置,为扫描器使用相机,为广告进行跟踪。
  • 新的 flavor 或 target。 免费版构建加入了付费版没有的广告 SDK。
  • 移除。 同样是偏差——政策现在声明过多,这是更安全的错误,但仍然是错误。

上面每一种都会落在某个构建文件中——build.gradle、AndroidManifest.xml、Podfile、Package.swift、Info.plist、pubspec.yaml、package.json。正是这一点让它可以被检查。

方法:一份记录,加上每个拉取请求的检查

  1. 页面写好时,记录它们是根据什么写的:当时构建中的 SDK、数据类型和权限。
  2. 每个拉取请求都重新读取构建文件并进行比对。
  3. 如果有不同,说明是什么、在哪里(文件和行号),以及它可能影响哪个页面或表单。

有两个特性很重要。检查必须只读取构建文件——从不读取源代码,从不读取机密信息——这样它在任何代码仓库上运行都是安全的。而且它必须说”可能已过时”,而不是”违规”:构建文件中有某个库,并不能证明 App 在使用它,只有你知道它是否改变了你的 App 收集的数据。

用 AppFoyer 进行设置

这个检查在所有方案中都免费。它只读取构建文件,不需要密钥或机密信息,不向任何地方发送任何内容,也从不修改页面。

使用 Claude Code 插件

  1. 说 “make this app store-ready”。除了起草页面,插件还会在代码仓库根目录写入 appfoyer.json:记录页面是根据什么写成的。
  2. 说 “add the AppFoyer drift check”。它会向你展示一个 GitHub workflow,在你同意后写入 .github/workflows/appfoyer.yml。
  3. 提交这两个文件。插件从不替你提交或推送。

不使用插件

自己添加 workflow:

name: AppFoyer drift check
on: pull_request
permissions:
  contents: read
jobs:
  check:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v5
      - uses: appfoyer/check-action@v1

打开一个拉取请求。如果代码仓库中还没有 appfoyer.json,作业摘要会显示它根据你的构建将要写入的文件。将它与你已发布的页面进行比对,然后提交。

或者在本地创建——同一个扫描器,发布在 npm 上:

npx @appfoyer/check init

在其他 CI 系统上

这个 npm 包可以在任何运行 Node 20 或更高版本的地方运行。以 GitLab 为例:

appfoyer-drift-check:
  image: node:22
  script:
    - npx -y @appfoyer/check --fail-on-drift

--format markdown 输出的报告与 GitHub 作业摘要中显示的相同;--format json 用于脚本。

检查结果是什么样的

一个拉取请求向一个 App 加入了 RevenueCat,而这个 App 的页面是在没有它的情况下写成的:

"Trail Notes": 2 changes
  + In-app purchases / subscriptions (data collected)  app/build.gradle.kts:28
      → Privacy policy may not declare "In-app purchases / subscriptions"
      → Google Play Data safety and Apple App Privacy answers may be outdated
      → Terms may not cover purchases and subscriptions
  + RevenueCat (SDK)  app/build.gradle.kts:28
      → Store forms: RevenueCat is not in the app's SDK list
      → Privacy policy may not name this SDK

默认情况下,作业只报告并通过。在 with: 下添加 fail-on-drift: "true",即可让它在记录更新之前一直失败。

当它报告变化时

  1. 更新受变化影响的内容。 在同一个分支上再说一次 “make this app store-ready”:插件会重新读取构建,更新 App 的事实,重新起草受影响的页面,并重写 appfoyer.json。或者自己在控制面板中编辑页面和 Store forms 标签页。
  2. 刷新记录。 插件已经做好了。如果是在控制面板中编辑的,运行 npx @appfoyer/check update,或者从作业摘要中复制刷新后的文件,它就在检查结果下方。
  3. 在同一个拉取请求中提交。 下一次运行将不显示任何变化,而历史记录会显示依赖变化和披露变化是一起到来的。
  4. 在控制台中更新商店表单。 检查和 AppFoyer 都无法访问 Play Console 或 App Store Connect。草稿答案在控制面板的 Store forms 标签页中。
  5. 在审阅界面中发布更新后的草稿。

一个代码仓库中有多个 App

appfoyer.json 为每个商店页面保存一个条目。拥有自己 application id 的 product flavor 会有自己的条目,因此只有免费 flavor 使用的广告 SDK 会与免费 flavor 的页面进行比对,而不是付费 flavor 的页面。插件会为它处理过的每个 flavor 写入一个条目。如果手动编写,条目可以有一个 sourceSets 列表——["free"],或者对于多维度构建,列出组合中的每个 flavor 以及组合本身——当 App 位于 monorepo 中时,还可以有一个 root 文件夹。

它看不到什么

  • 依赖的依赖,iOS 除外,因为 Podfile.lock 会列出它们。由另一个库引入的 SDK 不会出现在你的构建文件中,所以也不会出现在这里。
  • 仅存在于代码中的配置。 Expo 中的 app.config.js / app.config.ts 是 JavaScript,不会被读取;app.json 和 package.json 会被读取。
  • 你是否在使用你声明的东西。 存在但未使用的依赖仍然会被报告。
  • 你的后端。 构建文件中没有任何内容说明你的服务器存储什么或存储多久。

它是在披露成本最低的那一刻给你的提醒,不是合规结论,也不是法律建议。

权威来源