讓你的隱私權政策與建置保持同步
隱私權政策寫一次,而在某個 pull request 加入 SDK 的那天就過時了。如何直接在 pull request 上發現這件事:用一個只讀取建置檔案的免費檢查。
記錄你的頁面是根據哪些 SDK、資料類型和權限撰寫的,然後拿每個 pull request 的建置檔案和這份紀錄比對。當出現紀錄中沒有的相依套件或權限時,pull request 會在發布之前就顯示出來,那時隱私權政策和商店表單都還很容易更新。
改用幾分鐘完成這件事
AppFoyer 託管這篇指南所談的頁面:隱私權政策、服務條款、支援、帳戶刪除和 app-ads.txt,放在各自的子網域上,你不需要自己的網域或伺服器。一個應用程式免費,而且商店要求的任何內容都不會放在付費牆後面。
查核日期:2026年9月30日。 商店政策會變動,有時不會事先通知。以下所有內容都附上官方文件的連結,具有權威的是那個頁面,而不是本頁。如果兩者不一致,以商店為準。 本指南譯自英文;引用的商店文件保留英文原文。
撰寫隱私權政策是一次性的工作。讓它保持正確則不是:政策描述的是你撰寫當時的建置,而建置一直在變。某個 pull request 加入了當機回報、訂閱程式庫或位置權限,應用程式就這樣上線了,而隱私權政策、Data safety 表單和 App Privacy 詳細資料描述的仍是舊的應用程式。
兩家商店都把這視為你的問題,而不是它們的問題。Google Play 表示,你的 Data safety 答案必須隨時保持正確且完整,而你的應用程式中第三方程式庫和 SDK 收集的資料,也要由你申報。Apple 對 App Privacy 詳細資料的說法相同,並補充說你可以隨時在 App Store Connect 中變更答案,不必提交更新。
落差實際上是怎麼來的
幾乎從來不是因為決定要「收集更多資料」。它來自日常的工程工作:
- 新的相依套件。當機回報、分析、付費牆 SDK、地圖程式庫。
- 新的權限。某項功能用到的位置、掃描器用到的相機、廣告用到的追蹤。
- 一個 flavor 或 target。免費版建置多了一個付費版建置沒有的廣告 SDK。
- 移除。這也是落差:政策現在申報過多,這是比較安全的錯誤,但仍然是錯誤。
這些變動每一個都會出現在某個建置檔案中:build.gradle、AndroidManifest.xml、Podfile、Package.swift、Info.plist、pubspec.yaml、package.json。這正是它可以被檢查的原因。
做法:一份紀錄,加上每個 pull request 的檢查
- 撰寫頁面時,記錄它們是根據什麼寫的:當時建置中的 SDK、資料類型和權限。
- 每個 pull request 都重新讀取建置檔案並比對。
- 有不同之處時,說明是什麼、在哪裡(檔案和行號),以及可能影響哪個頁面或表單。
有兩項特性很重要。檢查必須只讀取建置檔案,絕不讀取原始碼,絕不讀取機密資訊,這樣才能安全地在任何儲存庫上執行。而且它必須說「可能已過時」,而不是「違規」:建置檔案中有某個程式庫,並不能證明應用程式有使用它,而它是否改變了你的應用程式收集的資料,只有你知道。
用 AppFoyer 設定
這項檢查在每個方案中都免費。它只讀取建置檔案,不需要任何金鑰或機密資訊,不會把任何東西傳送到任何地方,也從不變更頁面。
使用 Claude Code 外掛程式
- 說 “make this app store-ready”。除了起草頁面之外,外掛程式還會在你的儲存庫根目錄寫入
appfoyer.json:也就是頁面是根據什麼撰寫的紀錄。 - 說 “add the AppFoyer drift check”。它會給你看一個 GitHub 工作流程,並在你同意後寫入
.github/workflows/appfoyer.yml。 - 提交這兩個檔案。外掛程式從不替你 commit 或 push。
不使用外掛程式
自己加入工作流程:
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
開一個 pull request。如果儲存庫中還沒有 appfoyer.json,job summary 會顯示它根據你的建置將寫入的檔案。拿它和你已發布的頁面比對,然後提交它。
或是在本機建立它:使用同一個掃描器,已發布在 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 job summary 相同的報告;--format json 則供指令碼使用。
檢查結果長什麼樣子
一個 pull request 為一個頁面撰寫時沒有 RevenueCat 的應用程式加入了 RevenueCat:
"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",就能讓它在紀錄更新之前都判定失敗。
回報變更時怎麼做
- 更新受變更影響的內容。在同一個分支上再說一次 “make this app store-ready”:外掛程式會重新讀取建置、更新應用程式的事實、重新起草受影響的頁面,並重寫
appfoyer.json。或者自己在儀表板中編輯頁面和 Store forms 分頁。 - 更新紀錄。外掛程式已經做好了。在儀表板中編輯後,執行
npx @appfoyer/check update,或從 job summary 中複製更新後的檔案,它就在檢查結果的下方。 - 在同一個 pull request 中提交。下一次執行就不會顯示任何變更,而歷史紀錄會顯示相依套件和揭露內容的變更是一起進來的。
- 在主控台中更新商店表單。這項檢查和 AppFoyer 都無法連到 Play Console 或 App Store Connect。草稿答案在儀表板的 Store forms 分頁中。
- 從審查畫面發布更新後的草稿。
一個儲存庫中有多個應用程式
appfoyer.json 為每個商店資訊頁保存一筆項目。有自己 application id 的 product flavor 會有自己的項目,所以只有免費 flavor 使用的廣告 SDK,會拿來和免費 flavor 的頁面比對,而不是付費版的頁面。外掛程式會為它處理成可上架的每個 flavor 各寫一筆項目。手動撰寫時,一筆項目可以有一個 sourceSets 清單(["free"],或對於多維度建置,列出組合中的每個 flavor 以及該組合本身),當應用程式位於 monorepo 中時,還可以有一個 root 資料夾。
它看不到什麼
- 相依套件的相依套件,iOS 除外,因為
Podfile.lock會列出它們。由其他程式庫帶進來的 SDK 不會出現在你的建置檔案中,所以也不會出現在這裡。 - 只存在於程式碼中的設定。Expo 中的
app.config.js/app.config.ts是 JavaScript,不會被讀取;app.json和package.json則會。 - 你是否使用了你宣告的東西。存在但未使用的相依套件仍會被回報。
- 你的後端。建置檔案中沒有任何內容能說明你的伺服器儲存什麼、保存多久。
它是在變更最容易揭露的時刻提醒你,不是合規判定,也不是法律建議。
權威來源
- 為 Google Play 的 Data safety 部分提供資訊 — Google Play Console 說明:讓表單保持正確,以及第三方 SDK 的資料
- App Store 上的 App 隱私權詳細資料 — Apple:讓回答保持最新,以及不必更新應用程式就能更新回答
appfoyer/check-action和@appfoyer/check— Action 和 npm 套件,以及它們讀取的內容