Google Play 警告消不掉:Fastlane 上傳 Native Debug Symbols 的三個陷阱

那個怎麼都消不掉的警告 Google Play Console 上掛著一則警告:「App Bundle 含有原生程式碼,而您尚未上傳偵錯符號檔」。 先釐清幾個名詞,因為 Google Play 的警告訊息把不同的東西混在一起講: 檔案 用途 來源 Native Debug Symbols (.so) 還原 C/C++ native crash 的堆疊追蹤 Flutter 引擎、NDK、第三方 SDK R8 Mapping (mapping.txt) 還原 Java/Kotlin 被 R8 混淆後的類別名稱 Gradle build(minifyEnabled true) 兩者都要上傳,Google Play 才不會抱怨。Flutter 專案兩者都需要:引擎帶了 .so,Gradle 開了 R8。 整體流程:Build → Detect → Upload 在看個別陷阱前,先理解正確的 CI 流程應該長什麼樣: 關鍵原則:上傳邏輯綁定 build 產物,不綁定部署環境。只要 build 裡有 .so 或 mapping.txt,就上傳。不管是 staging 還是 production。 但從這個目標到實際跑通,踩了三個完全不同性質的坑。 陷阱一:File.expand_path 的基準不是你以為的那個 第一版的 Fastfile 寫死了路徑: ...

February 24, 2026 · 2 分鐘 · Peter

後端修好了,前端卻沒跟上:Vue 網頁版的 JWT Refresh 機制從零補上

結帳頁突然報錯:Missing or invalid credentials 使用者在 App 內開啟網頁版結帳頁,輸入折扣碼後收到一個刺眼的錯誤訊息:「Missing or invalid credentials」。 不是每個人都遇到,也不是每次都發生。但只要在頁面上停留超過一段時間再操作,就一定會觸發。 聽起來很熟悉?沒錯,這跟前一篇文章的問題同源——JWT 15 分鐘過期。差別在於,上次是後端的 refresh API 被權限系統擋掉,這次是前端根本沒有呼叫 refresh 的能力。 架構背景:App 開網頁的 SSO 流程 我們的 Flutter App 會透過 in-app browser 開啟 Vue 網頁。token 的傳遞方式很直接: https://web.example.com/checkout/xxx?token=eyJhbG... Vue 從 URL query param 取得 JWT,存進 sessionStorage,之後的 API 呼叫都帶這個 token。 問題在於:只傳了 JWT,沒有傳 refresh token。 JWT 有 15 分鐘的壽命。使用者開啟頁面、慢慢填寫發票資料、比較方案——15 分鐘一晃就過了。接下來任何 API 呼叫都會收到 401,但 Vue 手上沒有 refresh token,無法換新的 JWT。只能眼睜睜看著使用者被擋在門外。 為什麼 Flutter 不需要擔心,但 Vue 必須處理 Flutter App 本身有完整的 token refresh 機制:interceptor 攔截 401、用 refresh token 換新 JWT、重送失敗的 request。整個流程無縫,使用者無感。 ...

February 24, 2026 · 3 分鐘 · Peter

Docker BuildKit Cache Mount 的隱形陷阱

症狀:部署成功,程式碼卻是舊的 一個新功能已經 commit 並推上 Git,Jenkins pipeline 顯示建置成功、部署完成,Kubernetes Pod 也順利啟動。進入 Pod 檢查——程式碼還是舊的。 這不是網路延遲、不是 image pull policy、不是 tag 衝突。問題藏在 Dockerfile 裡一行看起來「很聰明」的快取優化。 背景知識:Docker 建置的關鍵概念 在進入除錯過程之前,先釐清幾個 Docker 建置中的核心概念: Docker Image 與 Layer(映像檔與分層) Docker 映像檔由多個唯讀的「層」(layer)堆疊而成。Dockerfile 裡的每一條指令(如 COPY、RUN)都會產生一層。Docker 會對每一層計算 hash,下次建置時如果指令和輸入都沒變,就直接重用該層——這就是 layer cache。 BuildKit BuildKit 是 Docker 18.09 之後引入的新一代建置引擎(透過 DOCKER_BUILDKIT=1 啟用)。相比傳統引擎,BuildKit 支援平行建置、更聰明的快取策略,以及本文主角——--mount=type=cache(cache mount)語法。 Cache Mount(--mount=type=cache) BuildKit 專有的功能。它在 RUN 指令執行期間,將一個持久化的目錄掛載到容器內的指定路徑。這個目錄由 BuildKit 管理,不會被寫入最終的 image layer,但內容會跨越不同次的 docker build 保留下來。常見用途是快取套件管理器的下載目錄(如 yarn cache),避免每次建置都重新下載。 Inline Cache 與 --cache-from 另一種 BuildKit 快取策略。透過 --build-arg BUILDKIT_INLINE_CACHE=1 把快取 metadata 嵌入產出的 image,再用 --cache-from 從遠端 registry 拉取。這讓不同機器(例如 CI runner)也能共享建置快取。 ...

February 18, 2026 · 2 分鐘 · Peter

Flutter iOS 冷啟動閃退:Debug 模式的隱藏陷阱

問題現象:Xcode 正常,冷啟動閃退 最近在開發 Flutter iOS App 時遇到一個詭異的問題:透過 Xcode 按下 Run 按鈕啟動 App 完全正常,但當我停止 Debugger、從多工畫面滑掉 App、再從主畫面點擊圖示重新開啟時,App 只顯示 Launch Screen 就立刻閃退。 這個問題讓我走了不少彎路,最後發現答案簡單得令人意外。 誤導方向:UIScene 與 Plugin 註冊 由於 Xcode 26 開始顯示 UIScene lifecycle will soon be required 警告,我最初懷疑是 UIScene 遷移不完整導致的問題。接著又懷疑是 Plugin 註冊的 race condition,甚至在 GeneratedPluginRegistrant.m 中加入大量 debug log 來追蹤每個 Plugin 的註冊狀態。 嘗試過的「修復」包括: 完整實作 SceneDelegate 與 FlutterSceneDelegate 在 Flutter Engine 初始化前加入 0.5 秒延遲 逐一排除可能有問題的 Plugin 這些方向全部是錯的。 找到根本原因:iOS Console Log 關鍵轉折點是查看 iOS 的 Console log。使用 Xcode 的 Devices and Simulators > Open Console 或 macOS 的 Console.app 連接 iPhone,重現冷啟動閃退後,看到了這段關鍵訊息: ...

February 17, 2026 · 2 分鐘 · Peter

功能部署後憑空消失?一場 Jenkins Workspace 的除錯之旅

問題:功能「先在後不在」 我們替 Strapi 後台新增了 MFA(Multi-Factor Authentication)雙因素認證功能。第一次部署到 STG 時一切正常——登入攔截、TOTP 驗證、QR Code 設定流程都運作良好。 但幾天後,因為其他功能的修改推了新的 git tag stg-1.24,部署流程順利完成,Jenkins 顯示綠燈——打開 STG 後台,MFA 的登入流程卻完全不見了。 沒有錯誤訊息、沒有 crash log,功能就這樣無聲無息地消失。 這比「功能從未出現」更令人困惑:明明之前部署是好的,程式碼也沒有人動過 MFA 相關的部分,為什麼會突然不見? 調查過程:逐層排除 確認 Git Tag 內容 第一個直覺是——程式碼真的有被包進 tag 嗎? # 確認 tag 指向的 commit 包含 MFA 程式碼 git show stg-1.24:src/index.ts | grep -i mfa 結果確認 src/index.ts 中有 import { registerMfaRoutes } from './mfa/controller',src/mfa/ 目錄也完整存在。Git tag 本身沒問題。 檢查 Pod 內的實際檔案 既然 tag 正確,問題可能出在建置或部署階段。直接進到 Kubernetes Pod 裡看: # 進入 STG pod 檢查 kubectl exec -it deployment/strapi-stg -- sh # 檢查編譯後的檔案 ls dist/src/mfa/ # 結果:ls: dist/src/mfa/: No such file or directory # 檢查原始碼 ls src/mfa/ # 結果:ls: src/mfa/: No such file or directory MFA 相關檔案完全不存在於 Pod 中。 甚至連原始碼都沒有被複製進 Docker image。 ...

February 12, 2026 · 3 分鐘 · Peter

升級 Strapi v5.31+ 後 JWT 過期就被登出?問題藏在你沒注意到的內建路由裡

症狀:App 開啟 15 分鐘後被強制登出 使用者回報一個詭異的問題:App 正常使用一段時間後,突然被強制登出。重新登入後又好了,但過一陣子又被踢出去。 時間規律很明確——正好 15 分鐘。這恰好是我們 JWT 的過期時間。 直覺反應是去查前端的 token refresh 邏輯:interceptor 有沒有正確攔截 401?refresh token 有沒有正確儲存?重試機制有沒有 bug? 查了一輪,全部正常。 錯誤的除錯方向 這個問題前前後後花了不少時間,方向一直在前端打轉: 排查方向 結果 Flutter interceptor 邏輯 正常,有攔截 401 並觸發 refresh Token 儲存機制(SecureStorage) 正常,refresh token 有正確保存 JWT 過期時間設定 確認是 15 分鐘,符合預期 網路連線問題 排除,其他 API 都正常 每個環節看起來都沒問題,但結果就是 refresh 失敗。問題出在哪? 轉折點:看 HTTP Status Code 直到有人(或者說,終於有人)去看了 /auth/refresh 實際回傳的 HTTP status code: # JWT 過期後呼叫 refresh curl -X POST https://api.example.com/api/auth/refresh \ -H "Content-Type: application/json" \ -d '{"refreshToken": "valid-refresh-token-here"}' # 預期:200 OK(新的 JWT) # 實際:403 Forbidden 403,不是 401。 ...

February 6, 2026 · 2 分鐘 · Peter

AWS S3 Upload Failed: The Bucket Does Not Allow ACLs

問題發生 在將 Production 環境複製到 Staging 環境後,發現 Strapi CMS 無法上傳圖片到媒體庫,畫面只顯示 Internal Server Error。 Strapi 是一個開源的 Headless CMS(無頭內容管理系統),可以讓開發者快速建立 API,並提供管理後台來管理內容。在這個專案中,我們使用 Strapi 搭配 AWS S3 來儲存上傳的圖片和檔案。 追查過程 第一步:查看 Kubernetes Logs 由於 Strapi 部署在 EKS(Elastic Kubernetes Service,AWS 的託管 Kubernetes 服務)上,我透過 kubectl 指令查看 Pod 的日誌: kubectl logs -f deployment/strapi-stg --tail=100 kubectl 是 Kubernetes 的命令列工具,用來與 Kubernetes 叢集互動。logs 指令可以查看容器的輸出日誌。 第二步:找到錯誤訊息 在日誌中發現關鍵錯誤: error: The bucket does not allow ACLs AccessControlListNotSupported: The bucket does not allow ACLs 這個錯誤訊息指出 S3 Bucket(AWS 的物件儲存服務中的儲存桶)不允許使用 ACL。 ...

January 30, 2026 · 3 分鐘 · Peter

資料庫同步的隱藏陷阱:Link Table 的重要性

問題現象:登入成功卻被拒於門外 最近在 Staging 環境遇到一個詭異的問題:使用者登入成功,拿到了有效的 JWT Token,但存取任何需要認證的 API 都回傳 401 Unauthorized。 # 登入成功,拿到 token POST /api/auth/local → 200 OK { "jwt": "eyJhbGc...xxxxx...your-jwt-token", "user": { "id": 1001, "email": "user@example.com" } } # 但存取個人資料失敗 GET /api/users/me → 401 Unauthorized Token 驗證通過、使用者存在、帳號未被封鎖。問題到底在哪? 根本原因:遺失的 Link Table 經過一番追查,發現問題出在資料庫同步時漏掉了關聯表(Link Table)。 什麼是 Link Table? 在關聯式資料庫中,多對多關係需要透過中間表來建立。這個中間表就是 Link Table(也稱為 Junction Table、Join Table、或 Pivot Table)。 使用者與角色的關係: 一個使用者可以有多個角色(User → Roles) 一個角色可以分配給多個使用者(Role → Users) 這是典型的多對多關係 各種 ORM 的 Link Table 命名 不同框架的 Link Table 命名慣例不同,但概念完全相同: ORM/Framework Link Table 範例 備註 Django user_groups, user_permissions 使用 _ 連接 Laravel role_user, permission_role 字母順序排列 TypeORM user_roles_role 較長的命名 Prisma _UserToRole 以 _ 開頭 Sequelize UserRoles 駝峰命名 問題的本質:資料不完整 當我們同步資料庫時,通常會注意主要的資料表: ...

January 20, 2026 · 4 分鐘 · Peter

Flutter CI/CD Debugging: Three Build Failures in One Day

前言:當建置一直紅燈 CI/CD pipeline 亮紅燈是開發日常,但連續遇到三個不同層面的問題,從 iOS codesigning、Android Gradle、到 Google Play API,這就值得記錄下來了。 這篇文章記錄我在同一天內遇到的三個建置失敗,以及逐步排除的過程。每個問題都有其獨特的根因,但也反映出 CI/CD 環境的複雜性。 問題一:iOS Keychain 解鎖失敗 症狀 Jenkins 建置在 iOS 階段失敗,錯誤訊息: [!] Error unlocking keychain at path: fastlane_keychain Command failed with exit status 51 macOS Keychain 運作機制 在深入問題之前,先了解 macOS Keychain 的運作方式: 關鍵概念: macOS 可以有多個 Keychain,每個都有獨立密碼 憑證必須在「已解鎖」的 Keychain 中才能被 codesign 使用 CI 環境通常會建立專用的 Keychain,避免影響系統 Keychain Fastlane Match 與 Keychain 的互動流程 調查過程 Exit status 51 代表「密碼錯誤」。SSH 進 Jenkins Mac mini 確認: ...

January 17, 2026 · 2 分鐘 · Peter

AWS 跨區域遷移後的技術債清理:Strapi URL 的隱藏陷阱

接手專案,先看帳單 因為老闆信用卡到期了要換新卡,我順便看了一下 AWS 帳單金額,發現比預期高。之前詢問外包商技術長(已離職),得到的回覆是:「服務都已經從新加坡遷移到台北了,除了 S3 有保留做備份,其他都刪除了。」 身為工程師,最不能接受的就是「應該是這樣」。我決定親自盤點。 名詞解釋 在繼續之前,先解釋一下會提到的 AWS 服務: 服務 說明 費用特性 S3 (Simple Storage Service) 物件儲存服務,用來存放檔案、圖片、影片 按儲存容量和請求次數計費 NAT Gateway 讓私有子網路的資源能存取網際網路 按小時計費,即使沒流量也要錢 Elastic IP 固定的公開 IP 位址 使用中免費,未關聯則收費 VPC (Virtual Private Cloud) 虛擬私有網路,隔離你的雲端資源 VPC 本身免費,但相關資源收費 Network Load Balancer 負載平衡器,分散流量到多台伺服器 按小時和處理的資料量計費 ECR (Elastic Container Registry) Docker 映像檔儲存庫 按儲存容量計費 重點是:有些資源即使沒有流量,只要存在就會收費。NAT Gateway 和未關聯的 Elastic IP 就是典型的「隱形殺手」。 盤點遺留資源 # 檢查 EKS 叢集(Kubernetes 服務) aws eks list-clusters --region ap-southeast-1 # 結果:空的 ✓ # 檢查 RDS(資料庫) aws rds describe-db-instances --region ap-southeast-1 # 結果:空的 ✓ # 檢查 NAT Gateway aws ec2 describe-nat-gateways --region ap-southeast-1 \ --filter "Name=state,Values=available" # 結果:2 個還在跑 完整盤點結果: ...

January 10, 2026 · 3 分鐘 · Peter