Bỏ qua tới nội dung

Kỹ thuật

Báo lỗi trùng lặp: vì sao một lỗi đến hai lần, còn 20 lỗi chỉ đến một

Trong bài thử, báo một lỗi rồi throw lại không làm nó nhân đôi. Báo rồi throw một lỗi bọc thì có. Sáu kiểu code, đếm bằng SDK browser của Sentry và SDK của chính mình.

Andrew Babo6 phút đọc

Chia sẻ
Trong bài này
Báo lỗi trùng lặp: vì sao một lỗi đến hai lần, còn 20 lỗi chỉ đến một

Báo một lỗi rồi throw lại không làm nó bị gửi hai lần. Bọc nó trong một lỗi mới thì có. Và hai mươi lần throw giống hệt nhau liên tiếp có thể đến nơi thành một event, trong khi hai lỗi thay phiên nhau lại đến đủ hai mươi lần.

Đó là số đếm từ một bài thử mình chạy ngày 26/10/2026: sáu kiểu báo lỗi, mỗi kiểu trên một trang mới trong Chromium 141, ba lần mỗi kiểu, với SDK browser của Sentry để mặc định (@sentry/browser 10.76.2), cùng SDK đó nhưng tắt integration Dedupe, và @belvador/browser 0.5.1 của chính mình. Lần chạy nào cũng ra cùng con số.

Mình đã đếm gì#

Số event nhận được cho sáu kiểu báo lỗi
Thí nghiệm 26/10/2026 · Chromium 141 · 3 lần mỗi cấu hình, cùng kết quả
Kiểu codeSentry mặc địnhSentry tắt DedupeBelvador
captureException(e), rồi throw e111
captureException(e) hai lần111
captureException(e), rồi throw new Error("checkout failed", { cause: e })222
captureException(e) trong .catch, rồi throw lại111
Cùng một lỗi 20 lần liên tiếp (mỗi lần một Error mới, giống hệt nhau)1201
Hai lỗi thay phiên nhau, 20 lần throw202020

Cùng một object thì chỉ báo một lần#

Báo lỗi rồi throw lại là kiểu code rất hay gặp: bạn muốn có event kèm ngữ cảnh của mình, và muốn chỗ gọi vẫn lỗi như trước. Nó không nhân đôi gì cả, kể cả khi đã tắt Dedupe.

Lý do nằm trong phần lõi của SDK. Mỗi lần capture, nó gọi checkOrSetAlreadyCaught trước, hàm này đánh dấu object lỗi bằng một thuộc tính ẩn __sentry_captured__, hoặc bỏ qua nếu dấu đó đã có. Lỗi được throw lại vẫn là object cũ, nên global handler bắt được nó sẽ thấy dấu và dừng.

Cũng trong mã nguồn đó có ghi giới hạn: kiểu nguyên thuỷ (primitive) thì không đánh dấu được. Một chuỗi bị throw không có thuộc tính nào để gắn. Mình không đếm trường hợp đó ở đây, nhưng đó thêm một lý do nữa để throw object Error, bên cạnh cái stack mà nó mang theo, như bài thử về rejection đã đo.

Dấu đó cũng chỉ nằm trên object đó, trong process đó. Khi TanStack Start dựng lại lỗi của server trong trình duyệt, bản sao không có dấu, và bài thử về loader nhận hai event cho một lỗi, cho đến khi server báo cho trình duyệt biết lỗi nào nó đã gửi.

Bọc lại là thành lỗi thứ hai#

new Error("checkout failed", { cause: e }) là cách làm tốt. MDN mô tả cause là chỗ để ghi “the specific original cause of the error”, tức nguyên nhân gốc của lỗi, và SDK gửi cả hai: event của lỗi bọc có hai exception trong chuỗi, lỗi bọc và lỗi gốc.

Nhưng lỗi bọc là một object mới, không có dấu, nên nếu trước đó đã gọi captureException(e) thì lỗi gốc xuất hiện hai lần: một lần riêng, một lần làm cause. Hãy chọn một chỗ để báo: hoặc capture lỗi gốc rồi throw lại nguyên như vậy, hoặc bọc nó và để lỗi bọc là lần báo duy nhất.

TypeScript
// Hoặc: báo ở đây, throw lại đúng lỗi đócatch (e) { captureException(e); throw e; }// Hoặc: bọc lại, và chỉ báo lỗi bọc (ở tầng trên, hoặc để không ai bắt)catch (e) { throw new Error("checkout failed", { cause: e }); }

Dedupe chỉ bỏ cái vừa đến ngay trước#

Tài liệu Sentry nói integration Dedupe “deduplicates certain events”, tức bỏ bớt một số event trùng, và “only compares stack traces and fingerprints”, tức chỉ so stack trace và fingerprint. Trên thực tế, nó so mỗi event với event ngay trước đó.

Hai mươi lần throw, giống hệt hay xen kẽ
Thí nghiệm 26/10/2026 · Chromium 141 · @sentry/browser 10.76.2 · 3 lần, cùng kết quả

Hai mươi lần throw giống hệt nhau liên tiếp chỉ thành một event. Tắt Dedupe thì đủ hai mươi. Hai lỗi thay phiên nhau, A B A B, thì lúc nào cũng là hai mươi event, vì không event nào giống hệt event ngay trước nó. Một vòng lặp hỏng cùng một kiểu thì yên ắng; một trang có hai widget hỏng chạy theo timer thì không.

Vậy Dedupe giúp quota của bạn khỏi bị một vòng lặp liên tục phát sinh lỗi ăn hết, điều đó có ích. Nhưng nó không phải gom nhóm. Hai mươi event của một lỗi vẫn phải nằm trong một issue, và đó là việc của công cụ theo dõi lỗi ở phía server, không phải của SDK.

Danh sách kiểm tra#

  1. Báo mỗi lỗi ở đúng một chỗ. Capture rồi throw lại đúng object đó, hoặc bọc lại và chỉ báo lỗi bọc.
  2. Throw object Error để SDK đánh dấu được.
  3. Giữ Dedupe bật ở trình duyệt. Trong ca vòng lặp, nó giảm 20 event xuống 1.
  4. Bản trùng giữa các process, như server và trình duyệt, cần một bước bàn giao riêng.
  5. Khi số đếm trông lạ, hãy tái hiện kiểu code đó một lần và đếm những gì đến nơi, trước khi đụng vào cách gom nhóm.

Belvador giúp ở đâu#

@belvador/browser giữ nguyên dấu đánh dấu và Dedupe của engine, nên ở đây nó đếm giống Sentry mặc định. Trên node, Belvador gom event thành issue bằng một thuật toán có đánh version, nên hai mươi event của một lỗi là một issue kèm số đếm, và version gom nhóm mới không bao giờ gom lại issue cũ. Cách cài có trong hướng dẫn SDK trình duyệt, còn trang React dành cho app React. Khi đã hết trùng, bước tiếp theo là quyết định lỗi nào đáng cảnh báo.

Đọc tiếp#

Andrew Babo

Nhà sáng lập

Thuộc bài hướng dẫn: Xử lý lỗi JavaScript trong ứng dụng do AI viết: lỗi thoát ra từ đâu

Mọi bài viết

Đọc tiếp trong tài liệu

Xem lỗi production của chính bạn như thế này

Belvador chạy trên máy chủ Hetzner của chính bạn: lỗi, cron, uptime và trace — dữ liệu vẫn là của bạn.