Product Management
Đăng nhập
ESC

Nhập từ khóa để tìm kiếm

↑↓ Di chuyển
Enter Mở
ESC Đóng

Bài 32 — Crash Reporting & Symbolication

Đã đọc 0/14 mục · còn khoảng 11 phút
Cách chúng tôi ghi nhận

Một mục được tính là đã đọc khi nó nằm giữa màn hình đủ lâu so với độ dài của nó (bảng và sơ đồ tính một nửa), bạn vẫn đang cuộn hoặc bấm phím, và bạn đã thấy tới cuối mục. Lướt nhanh hoặc để tab mở không được tính. Chúng tôi chỉ lưu thời gian đọc từng mục — không lưu gì khác.

Mở đầu — vì sao bài này quan trọng

Hãy tưởng tượng bạn là QA của một ứng dụng đặt đồ ăn đang chạy trên 200.000 thiết bị. Một buổi sáng thứ Hai, tỉ lệ đánh giá 1 sao trên Google Play tăng vọt, người dùng phàn nàn "app tự thoát khi bấm nút thanh toán". Nhưng khi bạn cầm chính chiếc điện thoại của mình mở app lên, mọi thứ chạy mượt mà. Không reproduce được. Đội dev hỏi bạn: "Crash ở đâu, dòng nào, thiết bị nào?" — và nếu bạn chỉ có thể trả lời "em không biết, chỉ thấy user kêu thôi", thì bạn đang thiếu một trong những kỹ năng quan trọng nhất của Mobile QA hiện đại.

Điểm khác biệt lớn giữa web và mobile là: trên web, khi có lỗi bạn còn có thể xem console log của trình duyệt. Trên mobile, ứng dụng đã được cài trên máy của hàng chục nghìn người mà bạn không bao giờ chạm tới được. Crash xảy ra "ngoài kia" — trên một chiếc Xiaomi Redmi ở Cần Thơ, một chiếc iPhone SE đời cũ ở Đà Nẵng — và bạn hoàn toàn mù thông tin trừ khi có một hệ thống thu thập crash tự động.

Bài học này dạy bạn hai thứ gắn liền nhau: Crash Reporting (thu thập báo cáo sự cố tự động từ thiết bị người dùng) và Symbolication (dịch những dòng crash log khó hiểu toàn địa chỉ bộ nhớ thành tên hàm, số dòng cụ thể mà con người đọc được). Nắm được hai kỹ năng này, bạn chuyển từ QA thụ động "chờ user báo lỗi" sang QA chủ động "phát hiện, khoanh vùng và ưu tiên sửa lỗi trước cả khi user phàn nàn".

Khái niệm cốt lõi

Crash là gì và tại sao không tự nhìn thấy được

Một crash là khi hệ điều hành buộc phải chấm dứt (kill) ứng dụng vì nó rơi vào trạng thái không thể tiếp tục: truy cập con trỏ null, out of memory, unhandled exception, hay vi phạm ràng buộc của OS. Trên Android thường là NullPointerException, OutOfMemoryError; trên iOS là các signal như SIGSEGV, SIGABRT, hoặc EXC_BAD_ACCESS.

Khác với ANR (Application Not Responding — app treo nhưng chưa chết) hay lỗi logic (app chạy nhưng ra kết quả sai), crash làm app biến mất khỏi màn hình ngay lập tức. Vấn đề là: crash log gốc nằm trong bộ nhớ thiết bị, và trừ khi bạn cắm cáp vào máy đó chạy adb logcat hay xem Xcode Device Logs, bạn không thể thấy nó. Với hàng nghìn user, điều này bất khả thi. Đó là lý do ta cần một SDK nhúng vào app, tự bắt crash và gửi về server.

Stack trace và vì sao nó bị "che mờ"

Khi crash xảy ra, hệ thống ghi lại stack trace — chuỗi các hàm đã được gọi dẫn đến điểm sập. Nhưng bản build release mà user tải về đã trải qua tối ưu hóa: mã bị minify/obfuscate (Android, qua R8/ProGuard đổi tên PaymentValidator.validate() thành a.b()) hoặc chỉ còn địa chỉ bộ nhớ dạng hex (iOS, do symbol bị strip để giảm dung lượng). Kết quả là stack trace thô trông như thế này:

0  MyApp  0x0000000102a4c8f0  0x102a40000 + 51440
1  MyApp  0x0000000102a4b1c4  0x102a40000 + 45508

Hoàn toàn vô nghĩa với con người. Symbolication chính là quá trình ánh xạ ngược những địa chỉ/tên rút gọn này về mã nguồn gốc: tên class, tên method, tên file, số dòng.

Ba công cụ crash reporting phổ biến

Firebase Crashlytics — miễn phí, thuộc hệ sinh thái Google Firebase, phổ biến nhất tại Việt Nam. Ưu điểm là dễ tích hợp, dashboard trực quan, tự động nhóm các crash giống nhau thành "issue", và tính chỉ số crash-free users (tỉ lệ người dùng không gặp crash). Đây thường là lựa chọn mặc định cho startup và team vừa/nhỏ.

Sentry — mã nguồn mở, hỗ trợ đa nền tảng (mobile, web, backend cùng một chỗ), có thể tự host (self-host) để dữ liệu không ra khỏi hạ tầng công ty — điểm này rất quan trọng với ngân hàng, fintech Việt Nam có yêu cầu bảo mật dữ liệu. Sentry mạnh về breadcrumbs (dấu vết các hành động trước crash) và context phong phú.

Microsoft App Center (Visual Studio App Center) — của Microsoft, tích hợp cả build, distribute và crash reporting. Lưu ý: Microsoft đã thông báo ngừng App Center (retire tháng 3/2025), nên với dự án mới bạn nên chọn Crashlytics hoặc Sentry; App Center chỉ còn gặp ở các dự án cũ.

Các chỉ số QA cần theo dõi

Không phải cứ có crash là hoảng loạn. QA giỏi biết đọc chỉ số để ưu tiên:

  • Crash-free users %: tỉ lệ người dùng không gặp crash trong một khoảng thời gian. Mục tiêu ngành thường là > 99,5%.
  • Crash-free sessions %: tính trên số phiên, luôn cao hơn con số theo user.
  • Số user bị ảnh hưởng của từng issue: một crash ảnh hưởng 30.000 user quan trọng hơn nhiều so với crash chỉ ảnh hưởng 2 user.
  • Velocity/trend: crash mới xuất hiện và tăng nhanh sau một bản release là dấu hiệu cần chặn rollout ngay.

Tình huống thực tế

Tình huống 1 — App đặt đồ ăn và crash chỉ trên Android 14

Một công ty giao đồ ăn giả định tên FoodFast tại TP.HCM phát hành bản v3.2. Sau 4 tiếng, dashboard Crashlytics báo crash-free users tụt từ 99,6% xuống 97,1%. Đội QA mở dashboard, thấy một issue chiếm 82% tổng số crash mới, với stack trace (đã symbolicate) trỏ vào NotificationChannelHelper.createChannel(), và metadata cho thấy 100% xảy ra trên Android 14, không có ca nào trên Android 13 trở xuống.

Nhờ breadcrumbs, họ thấy crash xảy ra ngay sau sự kiện "user cho phép quyền thông báo". Hóa ra Android 14 siết chặt API tạo notification channel, code cũ gọi sai. Vì có đầy đủ thông tin — phiên bản OS, dòng code, tần suất — team hotfix trong 3 tiếng và chặn rollout tại 20% người dùng nên chỉ một phần nhỏ bị ảnh hưởng.

Bài học rút ra: Crash reporting không chỉ nói "có lỗi" mà còn khoanh vùng chính xác ai bị ảnh hưởng (OS nào, thiết bị nào). QA phải biết đọc phân bố theo OS version và tận dụng staged rollout để giới hạn thiệt hại.

Tình huống 2 — iPhone crash với stack trace toàn số hex

Một ví bán hàng giả định ShopWallet nhận báo cáo iOS crash. QA mở Crashlytics thấy stack trace vẫn ở dạng thô: 0x0000000102a4c8f0 + 51440, không có tên hàm. Nguyên nhân: khi build bản release, đội dev quên upload file dSYM (debug symbols) lên Crashlytics, nên hệ thống không thể symbolicate.

QA phối hợp dev tải dSYM từ App Store Connect (Xcode strip symbol khi build với Bitcode/upload), upload thủ công qua script upload-symbols, và sau vài phút stack trace hiện rõ crash nằm ở CurrencyFormatter.format() khi gặp số tiền âm do lỗi hoàn tiền. Từ sự cố này, team thêm bước tự động upload dSYM vào pipeline CI/CD để không bao giờ tái diễn.

Bài học rút ra: Symbolication phụ thuộc vào file symbol (dSYM cho iOS, mapping.txt cho Android). Thiếu file này thì crash report vô dụng. QA nên coi "kiểm tra symbol đã được upload chưa" là một mục trong checklist release.

Tình huống 3 — Fintech tự host Sentry vì lý do bảo mật

Một fintech giả định VietPay không được phép gửi dữ liệu người dùng ra máy chủ nước ngoài theo chính sách nội bộ. Họ chọn self-host Sentry trên hạ tầng riêng. QA cấu hình data scrubbing để tự động loại bỏ thông tin nhạy cảm (số thẻ, số điện thoại, email) khỏi crash context trước khi lưu. Khi một crash liên quan màn hình OTP xuất hiện, breadcrumbs cho thấy chuỗi thao tác nhưng mọi trường nhạy cảm đều đã bị che thành [Filtered].

Bài học rút ra: Crash reporting có thể vô tình rò rỉ dữ liệu cá nhân qua log và context. QA phải kiểm thử cả khía cạnh riêng tư: đảm bảo PII (thông tin định danh cá nhân) không bị gửi kèm crash. Đây là điểm dễ bị bỏ sót nhưng cực kỳ quan trọng với ngành tài chính.

Hướng dẫn từng bước

Dưới đây là quy trình QA nên nắm để làm chủ crash reporting trên một dự án, lấy Crashlytics làm ví dụ chính (Sentry tương tự về ý tưởng).

Bước 1 — Xác nhận SDK đã được tích hợp đúng. Kiểm tra app đã thêm dependency Crashlytics/Sentry và khởi tạo ở tầng Application/AppDelegate. Đây là việc của dev nhưng QA cần verify bằng cách chủ động gây crash thử.

Bước 2 — Chủ động test crash (forced crash). Nhờ dev để lại một nút ẩn hoặc dùng lệnh gây crash cố ý (ví dụ Crashlytics có crashlytics().crash()). Chạy app bản test, bấm nút, đóng app, mở lại (Crashlytics chỉ gửi report ở lần mở app kế tiếp), rồi kiểm tra crash có lên dashboard trong vài phút không. Nếu không lên → hệ thống chưa hoạt động, phải fix trước khi release.

Bước 3 — Đảm bảo symbol được upload. Với Android: file mapping.txt (từ R8/ProGuard) phải được upload; thường plugin Gradle của Crashlytics làm tự động, hãy verify. Với iOS: file dSYM phải được upload sau mỗi build. Kiểm tra trên dashboard xem có cảnh báo "missing dSYM" không.

Bước 4 — Thêm custom keys và logs. Đề nghị dev gắn thêm ngữ cảnh hữu ích: user_type (guest/premium), screen_name, feature_flag đang bật. Những custom key này giúp bạn lọc và hiểu crash nhanh hơn rất nhiều khi điều tra.

Bước 5 — Đọc và phân loại crash trên dashboard. Với mỗi issue, xem: stack trace đã symbolicate, số user ảnh hưởng, phân bố theo OS/thiết bị/phiên bản app, breadcrumbs. Ưu tiên theo (số user ảnh hưởng × mức độ nghiêm trọng), không phải theo thứ tự thời gian.

Bước 6 — Gắn crash vào quy trình theo dõi. Kết nối Crashlytics/Sentry với công cụ quản lý bug (Jira, Slack). Cấu hình alert: khi một crash mới đạt ngưỡng X user trong Y phút sau release → gửi cảnh báo ngay để cân nhắc chặn rollout.

Bước 7 — Theo dõi sau khi fix. Sau khi dev sửa và release bản mới, "resolve" issue trên dashboard rồi theo dõi: nếu crash tái xuất hiện ở phiên bản mới, công cụ sẽ tự "regress" (mở lại) issue — đây là tín hiệu fix chưa triệt để.

Lỗi thường gặp & mẹo

Quên upload dSYM/mapping file — lỗi kinh điển nhất. Stack trace không đọc được thì crash report vô nghĩa. Mẹo: đưa việc upload symbol vào pipeline CI/CD tự động, và thêm dòng "verify symbolication" vào checklist release.

Nhầm crash với ANR — ANR (app treo, không phản hồi) là loại vấn đề khác, hiển thị riêng. Đừng gộp chung; phân tích ANR cần nhìn thread bị block chứ không phải stack trace exception.

Chỉ nhìn tổng số crash mà bỏ qua số user ảnh hưởng — một crash 5.000 lần từ 3 user (do bot/vòng lặp lỗi) ít nghiêm trọng hơn crash 5.000 lần từ 5.000 user. Luôn ưu tiên theo user impact.

Bỏ qua non-fatal errors — nhiều team chỉ quan tâm fatal crash. Nhưng bạn có thể chủ động log các exception đã bắt (recordException) để phát hiện lỗi ngầm chưa làm sập app nhưng phá vỡ trải nghiệm.

Không test rằng crash reporting thực sự hoạt động — nhiều dự án tưởng có SDK là xong, đến khi cần thì không có report nào. Luôn làm forced-crash test cho mỗi nền tảng (iOS và Android riêng) trước release lớn.

Rò rỉ PII qua log và breadcrumbs — kiểm tra kỹ những gì được gửi kèm. Bật data scrubbing, đừng để mật khẩu/OTP/số thẻ lọt vào custom log.

Mẹo tận dụng breadcrumbs: khi một crash khó reproduce, breadcrumbs (chuỗi màn hình + hành động trước khi sập) thường là manh mối vàng để bạn dựng lại kịch bản tái hiện lỗi.

Bài tập thực hành

  • Forced crash end-to-end: Lấy một app demo (hoặc app đang test), phối hợp dev nhúng Crashlytics, tự tạo một crash cố ý. Thực hiện đủ chu trình: gây crash → đóng/mở lại app → kiểm tra report lên dashboard. Viết lại thời gian từ lúc crash đến lúc report xuất hiện.
  • So sánh trước và sau symbolication: Chụp lại stack trace của một crash iOS khi chưa upload dSYM (toàn hex) và sau khi upload. Ghi chú sự khác biệt và giải thích cho một đồng nghiệp non-tech vì sao symbolication quan trọng.
  • Phân loại ưu tiên: Cho một danh sách giả định 5 crash với các con số (crash A: 12.000 user; B: 3 user nhưng ở màn thanh toán; C: 500 user chỉ trên 1 dòng máy cũ...). Hãy sắp xếp thứ tự ưu tiên xử lý và viết lý do cho từng lựa chọn.
  • Kiểm thử quyền riêng tư: Cấu hình một custom log rồi kiểm tra xem có trường nhạy cảm nào (email, số điện thoại) bị gửi lên không. Đề xuất quy tắc scrubbing.
  • Viết checklist release: Soạn một checklist "Crash reporting readiness" gồm tối thiểu 6 mục mà QA phải xác nhận trước mỗi lần phát hành.

Tóm tắt

Crash Reporting là hệ thống thu thập tự động các sự cố sập app từ hàng nghìn thiết bị người dùng ngoài thực tế — thứ mà bạn không bao giờ có thể tự tay quan sát hết. Ba công cụ chính là Firebase Crashlytics (miễn phí, phổ biến nhất ở Việt Nam), Sentry (mã nguồn mở, self-host được, hợp với fintech), và App Center (của Microsoft, đang ngừng hỗ trợ nên chỉ còn ở dự án cũ).

Symbolication là mảnh ghép không thể thiếu: nó dịch stack trace thô — toàn địa chỉ hex hoặc tên rút gọn — thành tên hàm và số dòng con người đọc được, dựa vào file dSYM (iOS) hay mapping.txt (Android). Thiếu bước này, mọi crash report đều vô dụng.

Vai trò của Mobile QA không dừng ở việc cài SDK, mà là: chủ động test crash reporting có hoạt động thật không, đảm bảo symbol luôn được upload, đọc dashboard theo user impact chứ không theo số lượng thô, tận dụng breadcrumbs để tái hiện lỗi khó, kiểm thử cả khía cạnh riêng tư để không rò rỉ PII, và gắn crash vào quy trình cảnh báo để chặn rollout kịp thời. Làm chủ được những điều này, bạn chuyển từ "chờ user kêu ca" sang "phát hiện và dập lỗi trước khi nó lan rộng" — đúng tinh thần của một QA hiện đại.

Học xong bài này rồi? Tạo tài khoản miễn phí để lưu lại — lần sau vào là biết ngay đang dở ở đâu, và học hết khóa thì có chứng chỉ. Lưu tiến độ của tôi