Đây là bài thực hành tổng hợp của toàn module: một bug production (lỗi xảy ra trên môi trường thật, đang phục vụ người dùng) phức tạp, khó reproduce (tái hiện lại lỗi), sẽ được điều tra từ đầu đến cuối — từ một ticket support vài câu mơ hồ, cho tới một bug report hoàn chỉnh, một bản tóm tắt cho stakeholder (bên liên quan không thuộc team kỹ thuật), và một verification checklist (danh sách kiểm tra xác nhận đã fix đúng) sẵn sàng chạy sau khi fix được deploy.
Case study xuyên suốt bài: NovaCart, một sàn thương mại điện tử thời trang, nhận được báo cáo rời rạc về việc một số khách hàng bị trừ tiền hai lần cho một đơn hàng. Kiến trúc liên quan gồm order-service (Node.js/Express — tạo đơn, điều phối luồng checkout) và payment-service (Java/Spring Boot — bọc quanh Stripe, xử lý charge thực tế). Trang, Senior QA Engineer, là người nhận ticket và chủ trì điều tra. Toàn bộ 10 step dưới đây là nguyên văn quy trình Trang đã dùng — bạn có thể copy prompt, đổi tên service/field theo hệ thống của mình và áp dụng lại ngay.
Làm sao để bắt đầu điều tra một bug từ một báo cáo thiếu thông tin, mơ hồ?
Phần lớn bug production không đến với bạn dưới dạng một stack trace gọn gàng. Chúng đến dưới dạng một ticket Zendesk ba câu, không có order ID, không có timestamp chính xác, và một câu ghi chú mơ hồ "có vẻ có thêm vài ticket tương tự". Đây là nguyên văn ticket Trang nhận được:
Ticket #48213 — NovaCart Support
Customer: le***n@gmail.com
Subject: Bị trừ tiền 2 lần
Nội dung khách hàng:
"Em đặt hàng bên mình xong bị trừ tiền 2 lần trong sao kê ngân hàng,
mà app chỉ hiện đúng 1 đơn hàng. Mong hỗ trợ hoàn tiền gấp ạ."
Order ID: (khách không cung cấp / không nhớ)
Thời điểm: "hôm qua, khoảng chiều tối"
Support note: Tuần này đã có thêm 2-3 ticket có nội dung tương tự,
chưa rõ có liên quan tới nhau hay không.
Sai lầm phổ biến nhất ở đây là nhảy thẳng vào query database tìm "double charge" mà không định hình rõ mình đang tìm gì, tìm ở đâu, và cần thêm thông tin gì để không lạc hướng. AI giúp tốt nhất ở chính giai đoạn này — không phải để trả lời "nguyên nhân là gì" (chưa ai biết), mà để biến một báo cáo mơ hồ thành một investigation brief (bản tóm lược điều tra) có cấu trúc.
Step 1: Tạo investigation brief từ một báo cáo sơ sài
Mục tiêu: Không đoán bừa root cause (nguyên nhân gốc) từ 3 câu ticket. Dùng AI để tách rõ: cái gì đã biết, cái gì chưa biết, câu hỏi nào cần làm rõ trước, và bước kiểm tra rẻ nhất nên làm đầu tiên.
Prompt:
Tôi vừa nhận một ticket support mô tả nghi vấn bug double-charge (khách bị
trừ tiền 2 lần cho 1 đơn hàng) trên hệ thống e-commerce NovaCart. Đây là
nguyên văn ticket:
[dán ticket #48213 nguyên văn ở trên]
Hệ thống liên quan: order-service (Node.js, tạo đơn hàng, điều phối checkout)
và payment-service (Java/Spring Boot, gọi Stripe để charge thẻ).
Ticket này rất thiếu thông tin. Hãy tạo một Investigation Brief gồm:
1. Known Facts — chỉ những gì ticket thực sự xác nhận, không suy diễn.
2. Unknowns / Ambiguities — những gì còn mơ hồ và tại sao nó quan trọng.
3. Clarifying Questions — câu hỏi cụ thể nên hỏi lại khách hàng hoặc support
team để thu hẹp phạm vi (ưu tiên câu hỏi rẻ, dễ trả lời nhanh).
4. Minimum Evidence Needed — artifact cụ thể (log, query, dashboard) cần
thu thập TRƯỚC KHI có thể đưa ra giả thuyết đáng tin.
5. Initial Hypotheses (Low Confidence) — 3-4 khả năng rộng, ghi rõ đây chỉ
là giả thuyết sơ khởi chưa có bằng chứng.
6. Recommended First Checks — 2-3 hành động cụ thể, rẻ, làm ngay được mà
không cần chờ trả lời từ khách hàng.
Output AI trả về (Investigation Brief v1):
## Known Facts
- 1 khách hàng báo cáo bị trừ tiền 2 lần trên sao kê ngân hàng.
- App/hệ thống chỉ ghi nhận 1 đơn hàng tương ứng.
- Support ghi nhận có 2-3 ticket tương tự trong tuần này (chưa xác nhận liên quan).
## Unknowns / Ambiguities
- Không có order_id -> chưa thể tra log trực tiếp.
- "Hôm qua, khoảng chiều tối" không đủ chính xác để khoanh vùng log window.
- Chưa rõ 2 lần trừ tiền là 2 charge riêng biệt (Stripe) hay 1 charge + 1
hold tạm (authorization) chưa được release — đây là 2 loại lỗi khác nhau
hoàn toàn về nguyên nhân.
- Chưa biết các ticket "tương tự" có chung đặc điểm gì (platform, payment
method, khung giờ, có retry mạng hay không).
## Clarifying Questions
1. Gửi khách hàng: xin ảnh chụp sao kê ngân hàng (che số thẻ) — cần thấy
2 dòng transaction có timestamp và amount để phân biệt "2 charge thật"
với "authorization hold chưa release".
2. Hỏi support: 2-3 ticket tương tự đó có order_id không? Có thể tra ra
email/user_id được không?
3. Hỏi khách: đặt hàng bằng app mobile hay web? Có gặp lỗi mạng / màn hình
loading lâu / phải bấm đặt hàng lại lúc checkout không?
## Minimum Evidence Needed
- Query database/payment gateway: tìm mọi order có >1 successful charge
trong 14 ngày gần nhất, không chỉ riêng ticket #48213.
- Nếu tìm được order_id của khách: log order-service và payment-service
quanh order đó.
## Initial Hypotheses (Low Confidence — chưa có bằng chứng)
1. Client-side retry: khách bấm "Đặt hàng" 2 lần do UI phản hồi chậm/không
rõ ràng đã submit.
2. Idempotency key (khóa đảm bảo 1 request không bị xử lý 2 lần) bị xử lý
sai giữa order-service và payment-service khi có retry (do network/timeout).
3. Webhook/event xử lý trùng: payment-service nhận webhook Stripe 2 lần và
xử lý logic charge 2 lần do thiếu deduplication.
4. Race condition (2 tiến trình cùng đọc/ghi 1 trạng thái) khi user double-click
nút thanh toán trên frontend.
## Recommended First Checks (làm ngay, không cần chờ khách trả lời)
1. Chạy query tìm tất cả order có ≥2 successful charge trong 14 ngày qua để
biết đây là 1 case đơn lẻ hay một pattern có quy mô.
2. Kiểm tra dashboard Stripe: filter theo "duplicate charge" hoặc so khớp
amount + customer trong cùng ngày.
Bản brief này ngay lập tức tách rõ một điểm quan trọng mà ticket gốc không nói: cần xác nhận đây là 2 charge thật (2 giao dịch Stripe riêng biệt) hay chỉ là 1 charge + 1 khoản tạm giữ chưa được nhả — hai lỗi này có nguyên nhân hoàn toàn khác nhau và brief đã buộc bạn kiểm tra điều đó trước khi đi xa hơn.
Step 2: Thu thập bằng chứng ban đầu theo hướng dẫn của brief
Mục tiêu: Thực thi đúng "Recommended First Checks" từ brief — không phải tự đoán query cần viết, mà nhờ AI soạn query dựa trên đúng câu hỏi brief đã đặt ra, sau đó chạy thật và mang kết quả về.
Prompt:
Dựa trên Investigation Brief v1, tôi cần thực hiện "Recommended First Check #1":
tìm mọi order có ≥2 successful charge trong 14 ngày gần nhất.
Schema liên quan (rút gọn):
- orders(id, user_id, status, total_amount, created_at, platform)
- payments(id, order_id, charge_id, status, amount, created_at, idempotency_key)
Hãy viết:
1. Một query SQL tìm order_id có ≥2 row trong payments với status='succeeded',
nhóm kèm số lần charge và tổng amount.
2. Một query thứ 2 nhóm các case tìm được theo platform, theo ngày, và theo
khoảng cách thời gian (giây) giữa 2 lần charge liên tiếp — để tôi thấy có
pattern hay không.
Output (AI soạn 2 query, Trang chạy trên read replica production):
-- Query 1: order có >=2 successful charge trong 14 ngày
SELECT order_id, COUNT(*) AS charge_count, SUM(amount) AS total_charged
FROM payments
WHERE status = 'succeeded' AND created_at >= NOW() - INTERVAL '14 days'
GROUP BY order_id
HAVING COUNT(*) >= 2;
-- Query 2: pattern theo platform + khoảng cách thời gian giữa 2 charge
SELECT o.platform, DATE(p1.created_at) AS charge_date,
EXTRACT(EPOCH FROM (p2.created_at - p1.created_at)) AS gap_seconds
FROM payments p1
JOIN payments p2 ON p1.order_id = p2.order_id AND p2.id > p1.id
JOIN orders o ON o.id = p1.order_id
WHERE p1.status = 'succeeded' AND p2.status = 'succeeded'
AND p1.created_at >= NOW() - INTERVAL '14 days';
Kết quả thực tế: 47 order bị double-charge trong 9 ngày (từ 2026-06-21 đến 2026-06-30), trên tổng khoảng 82.400 order trong cùng khung 9 ngày (~0,057%). Điểm đáng chú ý: 44/47 case xảy ra trên platform = 'mobile-android' hoặc 'mobile-ios', chỉ 3/47 trên web. Khoảng cách thời gian giữa 2 lần charge (gap_seconds) dao động 2–9 giây, tập trung quanh mốc 8 giây. Ticket #48213 của khách hàng gốc chưa xác định được order_id cụ thể, nhưng nằm trong đúng vùng thời gian và pattern này.
Step 3: Tinh chỉnh investigation brief với bằng chứng ban đầu
Mục tiêu: Đưa bằng chứng thật vừa thu thập trở lại cho AI, để brief tự thu hẹp từ 4 giả thuyết mơ hồ xuống còn 1-2 hướng có bằng chứng ủng hộ rõ ràng, và chỉ ra chính xác artifact nào cần lấy tiếp.
Prompt:
Đây là kết quả thực tế từ Recommended First Check #1 và #2:
- 47 order double-charge trong 9 ngày (2026-06-21 đến 2026-06-30), trên
~82.400 order cùng kỳ (~0,057%).
- 44/47 case xảy ra trên mobile (Android/iOS), chỉ 3/47 trên web.
- Khoảng cách giữa 2 lần charge liên tiếp: 2-9 giây, tập trung quanh 8 giây.
- Mốc bắt đầu xuất hiện (2026-06-21) trùng với ngày deploy gần nhất của
order-service: PR "refactor-checkout-retry-flow".
Hãy cập nhật Investigation Brief:
1. Xếp hạng lại 4 hypothesis ban đầu theo bằng chứng mới — loại bỏ hypothesis
không còn phù hợp, giữ/nâng confidence cho hypothesis phù hợp.
2. Giải thích cụ thể vì sao con số "~8 giây" và "chủ yếu mobile" là chi tiết
quan trọng.
3. Liệt kê chính xác artifact cần lấy tiếp để xác nhận hoặc bác bỏ hypothesis
hàng đầu.
Output (Investigation Brief v2 — trích phần quan trọng):
## Hypothesis Ranking (sau khi có bằng chứng)
#1 (High) — Idempotency key xử lý sai khi có retry sau timeout mạng.
8 giây rất gần với các ngưỡng timeout HTTP client phổ biến (thường
cấu hình 5-10s). Việc lỗi tập trung trên mobile phù hợp với giả thuyết
mạng chập chờn (3G/4G) gây timeout nhiều hơn so với kết nối WiFi/web
ổn định. Mốc bắt đầu 2026-06-21 trùng ngày deploy PR
"refactor-checkout-retry-flow" là tín hiệu rất mạnh — đây là nghi phạm
số một, cần xem diff của PR này ngay.
#2 (Low, gần như loại) — Double-click nút thanh toán trên UI.
Nếu là double-click thuần túy của người dùng, phân bố lẽ ra ngẫu nhiên
theo platform (người dùng web cũng double-click). Việc lệch mạnh về
mobile (44/47) và có pattern thời gian rất đều (~8s) không khớp với
hành vi người dùng ngẫu nhiên — hạ xuống Low.
#3 (Low) — Webhook Stripe xử lý trùng.
Chưa loại hoàn toàn nhưng gap 8s không khớp tốt với chu kỳ retry webhook
thông thường của Stripe (thường lùi xa hơn theo exponential backoff).
## Vì sao "8 giây" và "chủ yếu mobile" quan trọng
Đây không phải nhiễu — đây là dấu vân tay của một cơ chế retry có cấu hình
timeout cố định, kích hoạt nhiều hơn trên kết nối chậm/chập chờn (mobile).
Một race condition ngẫu nhiên (double-click) sẽ không tạo ra độ lệch platform
rõ và gap thời gian tập trung quanh 1 giá trị như vậy.
## Artifact cần lấy tiếp
1. Diff của PR "refactor-checkout-retry-flow" (order-service).
2. Log order-service quanh 47 order_id đã xác định được, tập trung vào các
HTTP call tới payment-service và giá trị idempotency_key gửi đi mỗi lần.
3. API contract / OpenAPI spec của payment-service cho endpoint charge, để
biết idempotency key được kỳ vọng dùng như thế nào.
Mẹo: Đừng vội hỏi AI "nguyên nhân là gì" khi mới có một ticket vài câu — hãy để AI giúp bạn liệt kê CẦN HỎI THÊM GÌ và CẦN KIỂM TRA GÌ TRƯỚC. Phần lớn effort lãng phí trong bug investigation không nằm ở bước phân tích, mà ở việc điều tra sai hướng ngay từ đầu vì thiếu một câu hỏi làm rõ đơn giản.
Làm sao để xây dựng investigation context theo kiểu lặp cùng AI?
Một investigation brief tốt cho bạn biết cần artifact gì. Bước tiếp theo là đưa từng artifact vào đúng lúc, đúng thứ tự, để AI xây dựng dần một bức tranh đầy đủ — thay vì ném tất cả log 9 ngày vào một prompt duy nhất và hy vọng AI tự lọc ra tín hiệu. Đây là kỹ thuật context engineering (kỹ thuật xây dựng ngữ cảnh) áp dụng cho điều tra bug: mỗi lượt hội thoại bổ sung đúng một loại bằng chứng mới, có nhãn rõ ràng, và AI luôn được yêu cầu đối chiếu với những gì đã biết trước đó.
Step 4: Lấy log của order-service và trình bày cho AI
Mục tiêu: Lấy log đúng, đủ, có cấu trúc cho đúng 47 order_id đã xác định — không phải nguyên khối log 9 ngày — rồi để AI tìm pattern chung giữa các case.
Prompt (soạn câu lệnh truy vấn log trước):
Tôi có danh sách 47 order_id bị double-charge (đính kèm list). Log
order-service được lưu trên Kibana, mỗi log entry có field: timestamp,
order_id, request_id, event, idempotency_key, http_status, latency_ms.
Hãy viết cho tôi filter query Kibana (KQL) để:
1. Lấy toàn bộ log có order_id thuộc danh sách 47 order_id này.
2. Chỉ lấy các event liên quan tới việc gọi payment-service
(event: "charge_request_sent" hoặc "charge_response_received").
3. Sắp theo order_id, sau đó theo timestamp tăng dần.
Sau khi chạy filter và export, Trang có log thực tế cho từng order. Log của chính order tương ứng ticket #48213 (order_id ORD-98213) như sau:
2026-06-29T18:14:02.331Z order-service event=charge_request_sent
order_id=ORD-98213 request_id=req-a1f9 idempotency_key=idem-7c2e1a
http_status=- latency_ms=-
2026-06-29T18:14:10.402Z order-service event=charge_response_received
order_id=ORD-98213 request_id=req-a1f9 idempotency_key=idem-7c2e1a
http_status=TIMEOUT latency_ms=8071
2026-06-29T18:14:10.588Z order-service event=charge_request_sent
order_id=ORD-98213 request_id=req-b83d2 idempotency_key=idem-f04b6e
http_status=- latency_ms=-
2026-06-29T18:14:11.994Z order-service event=charge_response_received
order_id=ORD-98213 request_id=req-b83d2 idempotency_key=idem-f04b6e
http_status=200 latency_ms=1406
Prompt phân tích pattern trên toàn bộ 47 case:
Đây là log order-service cho 47 order double-charge (đã trim, dán bên dưới,
nhóm theo order_id). Ví dụ điển hình cho ORD-98213:
[dán 4 dòng log ở trên]
Hãy phân tích:
1. Pattern chung xuất hiện ở tất cả/hầu hết 47 case là gì?
2. Giá trị idempotency_key giữa lần gọi thứ 1 và thứ 2 trong cùng 1 order
có giống nhau không? Điều đó có ý nghĩa gì?
3. Mối liên hệ giữa http_status=TIMEOUT của lần gọi đầu và việc có lần gọi
thứ 2 xảy ra ngay sau đó là gì?
Output AI trả về:
Pattern chung ở toàn bộ 47/47 case:
1. Luôn có đúng 2 lần "charge_request_sent" cho cùng 1 order_id, cách nhau
0.1-0.6 giây SAU KHI lần gọi đầu tiên timeout ở mốc ~8000ms.
2. idempotency_key của 2 request là KHÁC NHAU (idem-7c2e1a vs idem-f04b6e).
Đây là dấu hiệu rất quan trọng: idempotency key đúng ra phải giữ NGUYÊN
giữa các lần retry của cùng một hành động thanh toán để payment-service
nhận diện đây là "cùng một yêu cầu, thử lại" chứ không phải "yêu cầu mới".
Nếu key khác nhau, payment-service không có cách nào biết đây là retry.
3. Request thứ 1 timeout ở phía order-service (client timeout 8000ms) —
nhưng "timeout" không có nghĩa là payment-service/Stripe không xử lý.
Rất có thể request 1 vẫn đang được Stripe xử lý và sẽ thành công, chỉ là
order-service không đợi được response. order-service sau đó tự động gửi
request 2 với key mới, và nếu Stripe cũng xử lý thành công request 2 ->
kết quả là 2 charge thành công cho 1 lần đặt hàng.
Đây là dấu hiệu mạnh của một retry logic không idempotent (không đảm bảo
"gọi lại nhiều lần chỉ có hiệu lực như gọi 1 lần"). Cần kiểm tra: (a) code
sinh idempotency_key ở đâu trong order-service, và (b) payment-service có
cơ chế dedupe theo key hay không, dùng đúng cách chưa.
Step 5: Đối chiếu với API contract của payment-service
Mục tiêu: Đối chiếu hành vi thực tế của order-service (vừa phát hiện ở Step 4) với API contract mà payment-service công bố, để xác định lỗi nằm ở bên gọi (caller) sai cách dùng, hay bên nhận (payment-service) không tuân thủ đúng cơ chế idempotency của chính nó.
Prompt:
Đây là đoạn OpenAPI spec của payment-service cho endpoint charge:
paths:
/v1/charges:
post:
parameters:
- name: Idempotency-Key
in: header
required: true
description: >
Client-generated key. If a request with the SAME key is received
again within 24h, the cached response of the FIRST successful
attempt is returned and NO new charge is created. Caller MUST
reuse the same key across retries of the same logical payment
intent, and MUST generate a new key only for a genuinely new
payment intent.
Và đây là kết luận từ log order-service (Step 4): order-service sinh
idempotency_key MỚI cho mỗi lần gọi HTTP tới payment-service, bao gồm cả
khi lần gọi đó là retry sau timeout của cùng một order.
Hãy:
1. Xác định: lỗi nằm ở caller (order-service) hay ở payment-service?
2. Giải thích payment-service có đang hoạt động "đúng như thiết kế" không
trong tình huống này.
3. Chỉ rõ contract mismatch (lệch pha kỳ vọng) cụ thể là gì.
Output AI trả về:
1. Lỗi nằm ở caller — order-service. payment-service hoạt động đúng theo
contract: nó chỉ dedupe khi nhận được CÙNG một Idempotency-Key. Nó không
có (và theo thiết kế API idempotency chuẩn, không nên có) cách nào để tự
suy luận "2 request khác key nhưng cùng amount/customer thực ra là 1 ý
định thanh toán" — việc đó là trách nhiệm của caller.
2. payment-service đang hoạt động đúng như thiết kế. Đây KHÔNG phải bug ở
payment-service. Việc dedupe theo key hoạt động chính xác — vấn đề là
nó chưa từng nhận được key trùng để dedupe.
3. Contract mismatch: OpenAPI spec ghi rõ "Caller MUST reuse the same key
across retries of the same logical payment intent." order-service không
tuân thủ điều này — nó coi mỗi lần gọi HTTP là một payment intent mới và
sinh key mới mỗi lần, bất kể đó là lần gọi đầu hay là retry sau timeout
của cùng một order. Đây chính là root cause ứng viên hàng đầu: cần xem
code sinh idempotency_key trong order-service, cụ thể là đã được sinh ở
đâu trong luồng gọi (tại đầu session checkout — đúng — hay tại mỗi lần
gọi HTTP riêng lẻ — sai).
Mẹo: Khi bug liên quan tới 2 service, đừng chỉ đưa log — luôn kèm cả API contract/interface spec vào context. Bug dạng "lệch pha giữa 2 service" gần như luôn nằm ở khác biệt kỳ vọng giữa bên gọi và bên nhận, không phải lỗi logic nội bộ của một service đơn lẻ. Không có contract, AI (và cả bạn) rất dễ đổ lỗi nhầm cho service "trông có vẻ liên quan" nhất chứ không phải service thực sự sai.
Làm sao để chạy một root cause analysis đầy đủ, từng bước, cùng AI?
Đến đây, bằng chứng đã đủ để chạy một root cause analysis (RCA — phân tích nguyên nhân gốc) chính thức, thay vì tiếp tục dò từng mảnh. Một RCA prompt tốt được chia làm 2 phần tách biệt: phần recap toàn bộ bằng chứng đã có (không thêm phân tích mới ở phần này) và phần yêu cầu cụ thể (specific ask). Tách 2 phần này giúp AI không phải vừa nhớ lại context vừa suy luận trong cùng một nhịp suy nghĩ.
Step 6: Chạy root cause analysis chính thức
Mục tiêu: Chốt lại root cause bằng một prompt RCA chính thức, có recap đầy đủ, yêu cầu AI dựng chuỗi nhân quả (causal chain) từ nguyên nhân kỹ thuật tới triệu chứng người dùng nhìn thấy, và nêu rõ điều gì sẽ bác bỏ kết luận này nếu sai.
Prompt RCA gồm đúng 2 phần dưới đây — dán liền nhau thành một prompt duy nhất gửi cho AI:
Summary of Findings
- Ticket gốc: khách hàng NovaCart báo bị trừ tiền 2 lần cho 1 đơn hàng.
- Query production xác nhận: 47/82.400 order (~0,057%) bị double-charge
trong 9 ngày (2026-06-21 -> 2026-06-30). 44/47 case trên mobile.
- Log order-service cho thấy pattern cố định ở cả 47 case: request charge
đầu tiên timeout ở client sau ~8000ms, order-service tự động gửi request
thứ 2 sau 0.1-0.6s, với idempotency_key MỚI (khác key lần 1).
- Request 1 timeout ở phía client KHÔNG đồng nghĩa Stripe/payment-service
chưa xử lý xong — rất có thể request 1 vẫn thành công ở phía server.
- OpenAPI contract của payment-service quy định rõ: caller phải TÁI SỬ DỤNG
cùng idempotency key cho các lần retry của cùng một payment intent.
payment-service dedupe đúng theo key nhận được — nó hoạt động đúng thiết kế.
- Mốc bắt đầu xuất hiện bug (2026-06-21) trùng ngày deploy PR
"refactor-checkout-retry-flow" trên order-service.
- Diff PR đó (đã lấy từ Git) cho thấy: hàm sinh idempotency key được di
chuyển từ nơi khởi tạo checkout session (chạy 1 lần, giá trị ổn định suốt
session) vào TRONG hàm gọi HTTP charge (chạy lại mỗi lần gọi, bao gồm mỗi
lần retry) — mục đích ban đầu của refactor là "đơn giản hoá luồng retry",
nhưng vô tình phá vỡ tính ổn định của key.
Root Cause Analysis Request
Dựa trên toàn bộ Summary of Findings trên, hãy thực hiện root cause analysis:
1. Trình bày chuỗi nhân quả đầy đủ (causal chain), từ thay đổi code trong
PR "refactor-checkout-retry-flow" tới triệu chứng "khách bị trừ tiền 2
lần" mà người dùng cuối nhìn thấy. Đánh số từng bước.
2. Nêu rõ ĐÂU LÀ ĐIỀU KIỆN CẦN (necessary condition) để bug này xảy ra —
tức là những gì phải cùng xảy ra để một order bị double-charge, giải
thích vì sao chỉ 0,057% order bị ảnh hưởng chứ không phải toàn bộ.
3. Phát biểu root cause dưới dạng một câu khẳng định có thể bị bác bỏ
(falsifiable claim).
4. Nêu điều kiện gì, nếu quan sát được, sẽ CHỨNG MINH kết luận này SAI.
Kết quả AI trả về:
## Causal Chain
1. PR "refactor-checkout-retry-flow" di chuyển việc sinh idempotency_key
từ tầng checkout session (ổn định) vào trong hàm gọiHTTP charge (sinh
mới mỗi lần gọi) trong order-service.
2. Khi mạng ổn định và response về nhanh (<8s), luồng chỉ gọi charge đúng
1 lần -> chỉ có 1 key -> không có vấn đề. Đây là lý do đa số order
(99,943%) không bị ảnh hưởng.
3. Khi client (đặc biệt mobile trên mạng chập chờn) gặp latency cao,
client timeout ở ngưỡng cấu hình cứng 8000ms trước khi nhận được response
từ payment-service — dù request đó vẫn có thể đang/đã xử lý thành công ở
phía server.
4. order-service coi timeout là "request thất bại, cần retry", tự động gửi
lại request charge — nhưng vì bug ở bước 1, request retry này mang một
idempotency_key HOÀN TOÀN MỚI, không liên hệ gì với key của lần gọi đầu.
5. payment-service nhận request retry với key mới -> theo đúng contract, nó
coi đây là một payment intent MỚI, không có gì để dedupe -> xử lý và
charge thành công lần thứ 2.
6. Nếu cả 2 lần gọi (request gốc timeout + request retry) đều được Stripe
xử lý thành công ở phía server -> khách hàng bị trừ tiền 2 lần cho 1 đơn
hàng, đúng như ticket #48213 phản ánh.
## Necessary Conditions (giải thích vì sao chỉ 0,057% bị ảnh hưởng)
Bug chỉ xảy ra khi TẤT CẢ các điều kiện sau cùng đúng:
(a) Request charge đầu tiên tới payment-service phải BỊ CLIENT-SIDE TIMEOUT
(không phải lỗi thật ở server) — cần latency cao bất thường, thường do
mạng mobile chập chờn.
(b) Request đó, dù client coi là timeout, vẫn phải THỰC SỰ được Stripe xử
lý thành công ở phía server (nếu request 1 thật sự fail ở server, sẽ
không có double-charge, chỉ có 1 charge từ request retry — vô hại).
(c) order-service phải retry tự động (đúng theo thiết kế retry-on-timeout).
Vì cần (a) VÀ (b) cùng xảy ra — tức "trễ ở tầng network/client, nhưng vẫn
xử lý xong ở server" — nên tỷ lệ ảnh hưởng nhỏ (~0,057%), và tập trung ở
mobile vì mobile có tỷ lệ (a) cao hơn nhiều so với web.
## Root Cause Statement (falsifiable)
"Việc PR refactor-checkout-retry-flow di chuyển logic sinh idempotency_key
vào trong hàm gọi HTTP charge (thay vì giữ ổn định theo checkout session)
khiến mỗi lần retry sau timeout tạo ra một idempotency key mới, làm mất
hiệu lực cơ chế dedupe của payment-service, dẫn tới double-charge khi
request gốc timeout ở client nhưng vẫn được xử lý thành công ở server."
## Điều kiện bác bỏ (falsification test)
Nếu kiểm tra code trước ngày 2026-06-21 (trước PR) cho thấy idempotency_key
CŨNG đã được sinh mới mỗi lần gọi HTTP (tức PR không thay đổi hành vi này),
thì root cause trên bị bác bỏ — cần tìm nguyên nhân khác cho việc bug chỉ
xuất hiện từ 2026-06-21. [Trang đã kiểm tra: code trước PR sinh key 1 lần
tại session start — xác nhận root cause đứng vững.]
Step 7: Đánh giá quy mô và mức độ ảnh hưởng
Mục tiêu: Định lượng chính xác quy mô ảnh hưởng — số khách hàng, số tiền, thời gian rủi ro đã tồn tại — để xác định đúng severity/priority và biết cần reconciliation (đối soát, hoàn tiền) cho bao nhiêu case chưa được phát hiện qua support.
Prompt:
Root cause đã xác nhận: bug tồn tại từ khi deploy PR
"refactor-checkout-retry-flow" (2026-06-21) tới hiện tại (2026-06-30),
tổng cộng 9 ngày.
Dữ liệu đã có: 47 order double-charge xác nhận qua query, tổng số tiền bị
charge dư là $6.230 (trung bình $132,55/case). Query mới chỉ chạy trên
14 ngày gần nhất — chưa xác nhận liệu có case nào TRƯỚC ngày query chạy
mà support chưa nhận ticket (khách chưa để ý sao kê, hoặc đã tự âm thầm
tranh chấp (dispute) với ngân hàng).
Hãy giúp tôi:
1. Ước tính exposure window thực tế và liệu 47 case đã tìm được có phải
là toàn bộ, hay chỉ là phần khách hàng đã/sẽ tự phát hiện.
2. Đề xuất cách xác định chính xác 100% case bị ảnh hưởng (không chỉ dựa
vào support ticket).
3. Đánh giá mức độ nghiêm trọng (severity) và độ ưu tiên (priority) theo
khung: mức độ ảnh hưởng tài chính, rủi ro uy tín/khiếu nại, có liên quan
compliance (PCI/hoàn tiền) không.
4. Gợi ý hành động ngay (trong vòng 24h) khác với hành động fix triệt để.
Output AI trả về:
1. Exposure window: 2026-06-21 -> hiện tại = 9 ngày, và bug vẫn ĐANG diễn
ra (chưa fix), nên số case sẽ tiếp tục tăng mỗi ngày cho tới khi có
mitigation. 47 case tìm được là case CÓ 2 lần "successful" trong hệ
thống của NovaCart — đây là con số đầy đủ theo dữ liệu nội bộ, không
phụ thuộc vào việc khách có report hay không (vì query trực tiếp trên
payments table, không qua ticket). Tuy nhiên vẫn có rủi ro: nếu 1 trong
2 charge bị Stripe hoàn tự động (do fraud check) thì case đó sẽ không
còn ở trạng thái "2 successful" và bị query bỏ sót — nên 47 nên coi là
ngưỡng sàn (floor), không phải số tuyệt đối.
2. Để xác định chính xác hơn: đối chiếu trực tiếp Stripe dashboard/API theo
customer_id + amount + cửa sổ thời gian ±15 phút, kể cả các charge đã bị
refund/void sau đó (khách tự dispute) — vì phía Stripe giữ đủ lịch sử
giao dịch mà DB nội bộ NovaCart có thể không phản ánh đầy đủ nếu có
refund/reversal xảy ra sau đó.
3. Đánh giá:
- Severity: SEV-2 (không phải outage toàn hệ thống, nhưng ảnh hưởng
tài chính trực tiếp tới khách hàng + đang tiếp diễn hàng ngày).
- Priority: P1 — cần mitigation trong ngày, vì đây là lỗi gây thiệt hại
tài chính thực tế cho người dùng, không phải lỗi UX.
- Rủi ro uy tín: cao — "bị trừ tiền 2 lần" là loại lỗi khách hàng chia
sẻ công khai (review, mạng xã hội) nhiều hơn hầu hết loại bug khác.
- Compliance: không phát hiện vi phạm PCI trực tiếp (dữ liệu thẻ không
bị lộ), nhưng cần đảm bảo hoàn tiền đầy đủ, đúng hạn theo chính sách
hoàn tiền đã công bố với khách hàng.
4. Hành động 24h, TÁCH RIÊNG khỏi fix triệt để:
- Mitigation nhanh: tăng client timeout ở order-service (ví dụ 8s -> 20s)
để giảm tần suất kích hoạt điều kiện (a) trong lúc chờ fix chính thức
— đây KHÔNG phải fix, chỉ giảm tần suất xảy ra tạm thời.
- Chủ động hoàn tiền cho toàn bộ 47 case đã xác nhận, không chờ khách
tự report — giảm số ticket support và rủi ro uy tín.
- Thông báo nội bộ cho team Finance/Support về pattern này để họ nhận
diện nhanh các ticket tương tự sắp tới trong lúc fix đang được triển khai.
Mẹo: Luôn tách RCA prompt thành 2 phần rõ ràng — Summary of Findings (chỉ recap, không phân tích mới) và phần yêu cầu cụ thể. Trộn chung 2 phần này buộc AI vừa phải nhớ lại toàn bộ bằng chứng vừa phải suy luận trong cùng một nhịp, dễ bỏ sót chi tiết — giống như yêu cầu một kỹ sư vừa đọc lại hồ sơ vừa kết luận ngay trong một câu.
Làm sao để tạo ra một bug report đầy đủ, sẵn sàng cho developer, bằng AI?
Root cause đã có, quy mô đã định lượng — bước cuối là đóng gói mọi thứ thành các tài liệu đúng đối tượng đọc: một bug report kỹ thuật đầy đủ cho developer fix, một bản tóm tắt phi kỹ thuật cho stakeholder, và một checklist QA sẽ dùng lại sau khi fix được deploy. Ba tài liệu này dùng CHUNG một nguồn sự thật (toàn bộ investigation ở trên) nhưng trình bày khác nhau hoàn toàn theo người đọc.
Step 8: Tạo bug report đầy đủ cho developer
Mục tiêu: Biến toàn bộ investigation thành một bug report mà developer có thể nhận và bắt tay fix ngay, không cần hỏi lại QA thêm bất cứ điều gì.
Giống Step 6, prompt cho bước này gồm 2 phần: recap toàn bộ investigation (Complete Investigation Findings) và yêu cầu định dạng cụ thể (Bug Report Requirements). Dán 2 phần này liền nhau thành một prompt duy nhất.
Complete Investigation Findings
- [Ticket gốc #48213 + pattern 47/82.400 order double-charge, 9 ngày,
44/47 trên mobile — xem Step 1-3]
- [Log order-service: timeout 8000ms ở request 1, retry với idempotency_key
mới sau 0.1-0.6s — xem Step 4]
- [OpenAPI contract payment-service: yêu cầu tái sử dụng key cho retry,
payment-service hoạt động đúng thiết kế — xem Step 5]
- [Root cause statement + causal chain 6 bước + necessary conditions +
điều kiện bác bỏ đã xác nhận đứng vững — xem Step 6]
- [Scope: SEV-2/P1, $6.230 exposure xác nhận (sàn, có thể còn case chưa
phát hiện), bug đang tiếp diễn — xem Step 7]
- [File/line liên quan: order-service/src/payment/chargeClient.js,
hàm sendChargeRequest(), dòng sinh idempotency_key bằng uuidv4() được
gọi trực tiếp trong hàm này thay vì nhận key ổn định từ checkout session]
Bug Report Requirements
Hãy tạo bug report đầy đủ theo format sau, viết bằng tiếng Anh (chuẩn để
đưa vào Jira), mỗi field phải cụ thể, không dùng ngôn ngữ mơ hồ:
- Title (1 dòng, mô tả đúng bản chất, không chỉ mô tả triệu chứng)
- Severity / Priority (kèm 1 câu lý do)
- Environment (service, version/PR liên quan, platform ảnh hưởng)
- Summary (2-3 câu, đọc hiểu ngay không cần đọc gì khác)
- Root Cause (root cause statement + causal chain rút gọn)
- Steps to Reproduce (bước cụ thể, giả lập được: cách tạo timeout mạng
nhân tạo để trigger điều kiện, không cần chờ mạng thật chập chờn)
- Evidence (liệt kê artifact kèm tham chiếu, không cần dán lại toàn bộ)
- Affected Code Location (file, function, mô tả đoạn logic sai)
- Suggested Fix (mô tả hướng fix, KHÔNG viết code hộ, để dev tự quyết cách
implement theo convention hiện có)
- Regression Risk (điều gì cần test kỹ khi fix để không phá vỡ luồng khác)
- Scope & Impact (số liệu từ Step 7)
Output — Bug report hoàn chỉnh AI tạo ra:
Title: Retry after client timeout generates a new Idempotency-Key,
bypassing payment-service dedup and causing double-charge
Severity / Priority: SEV-2 / P1
Reason: Direct financial impact to end users, actively ongoing (not a
one-off incident), high reputational risk due to visible bank statement
impact.
Environment:
- Service: order-service (Node.js), function sendChargeRequest()
- Introduced by: PR "refactor-checkout-retry-flow", deployed 2026-06-21
- Affected platform: primarily mobile-android / mobile-ios (44/47
confirmed cases); web affected but far less frequently (3/47)
- payment-service: NOT at fault, behaves per its documented contract
Summary:
When a charge request to payment-service exceeds the 8000ms client
timeout in order-service, the client retries automatically — but the
retry generates a brand-new Idempotency-Key instead of reusing the
original one. If the timed-out request had actually succeeded server-side,
payment-service processes the retry as an unrelated new payment intent,
resulting in two successful charges for a single order.
Root Cause:
Idempotency-Key generation was moved from checkout-session initialization
(stable, one key per session) into sendChargeRequest() (regenerated on
every HTTP call, including retries) as part of PR
"refactor-checkout-retry-flow". This violates payment-service's documented
contract requiring the same key be reused across retries of one logical
payment intent. Confirmed via log analysis of 47 production cases and
verified against pre-PR code behavior (falsification check passed).
Steps to Reproduce:
1. In a local/staging environment, add an artificial delay (>8000ms)
to payment-service's /v1/charges handler for a specific test order
(e.g. via a debug header or a feature flag), simulating server-side
processing that outlasts the client timeout.
2. Trigger checkout for a test order from order-service.
3. Observe: order-service logs a client timeout at ~8000ms, then
automatically retries with a NEW idempotency_key (visible in logs).
4. If step 1's delayed request still completes successfully server-side
(check payment-service logs / Stripe test dashboard), both the
original and the retry will succeed, producing two charges for one
order_id.
5. Confirms bug if: 2 distinct charge_id values exist for 1 order_id,
with 2 distinct idempotency_key values in order-service logs.
Evidence:
- Query results: 47 double-charge orders, 9-day window (Step 2)
- order-service logs for ORD-98213 and 46 other order_ids showing the
timeout -> retry -> new key pattern (Step 4)
- payment-service OpenAPI spec, Idempotency-Key contract clause (Step 5)
- Git diff of PR "refactor-checkout-retry-flow" confirming key generation
location change (Step 6)
Affected Code Location:
File: order-service/src/payment/chargeClient.js
Function: sendChargeRequest()
Issue: `const idempotencyKey = uuidv4();` is called at the top of this
function, which executes on every HTTP call including retries. Prior to
the refactor, the key was passed in as a parameter, generated once at
checkout-session start.
Suggested Fix:
Restore a single, stable idempotency key per logical payment intent —
generated once when the checkout session/payment intent is created, and
passed into sendChargeRequest() (and any retry of it) as a parameter
rather than generated inside it. Do not regenerate the key on retry
under any circumstance within the same payment intent's lifecycle.
Regression Risk:
- Verify genuinely NEW payment intents (e.g. user explicitly cancels and
re-initiates checkout) still receive a fresh key — do not over-fix into
"always reuse key" for unrelated new attempts.
- Test the full retry path end-to-end, not just the happy path, since
this is precisely where the original bug was introduced.
- Confirm no other caller of sendChargeRequest() relies on the old
per-call key generation behavior.
Scope & Impact:
47 confirmed cases over 9 days (2026-06-21 to 2026-06-30) out of ~82,400
orders (~0.057%), totaling $6,230 in duplicate charges (avg. $132.55/case).
This is a floor estimate — bug is ongoing until fixed; count increases
daily. 44/47 cases on mobile platforms.
Step 9: Tạo bản tóm tắt cho stakeholder
Mục tiêu: Cùng nguồn sự thật, nhưng viết lại hoàn toàn cho người đọc không đọc code — Product Manager, Head of Support, có thể cả leadership — cần biết: chuyện gì xảy ra, ảnh hưởng ai, đang xử lý thế nào, khi nào xong.
Prompt:
Dựa trên bug report developer-grade ở trên, hãy viết một Stakeholder Summary
bằng tiếng Việt, KHÔNG dùng thuật ngữ kỹ thuật (không nói idempotency key,
HTTP timeout, v.v. — diễn giải bằng ngôn ngữ thường), tối đa 200 từ, gồm:
1. Chuyện gì đã xảy ra (1-2 câu).
2. Ai/bao nhiêu bị ảnh hưởng, thiệt hại bao nhiêu.
3. Đang xử lý thế nào, khi nào có fix chính thức.
4. Hành động khách hàng có cần làm gì không (nếu không, nói rõ luôn).
Output AI trả về:
Tóm tắt cho stakeholder — Bug trừ tiền 2 lần (double-charge)
Chuyện gì xảy ra: Khi mạng của khách hàng (chủ yếu trên app mobile) bị
chậm đúng lúc thanh toán, hệ thống của chúng ta có thể vô tình gửi yêu cầu
thanh toán lần thứ hai trong khi lần đầu vẫn đang được xử lý bình thường —
dẫn tới khách bị trừ tiền 2 lần cho 1 đơn hàng.
Ảnh hưởng: 47 đơn hàng được xác nhận bị ảnh hưởng trong 9 ngày qua (từ
2026-06-21), tổng số tiền bị trừ dư khoảng $6.230. Đây là con số tối
thiểu — team đang đối soát thêm với Stripe để đảm bảo không sót case nào.
Hầu hết case xảy ra trên app mobile, ít gặp trên web.
Xử lý: Nguyên nhân kỹ thuật đã được xác định chính xác. Trong lúc chờ fix
chính thức, team đã triển khai một biện pháp giảm thiểu tạm thời để hạn chế
tần suất xảy ra. Fix triệt để đang được lên kế hoạch triển khai, kèm bộ
test đảm bảo không lặp lại vấn đề này ở các luồng thanh toán khác.
Hành động với khách hàng: Không cần khách hàng làm gì. Team sẽ CHỦ ĐỘNG
hoàn tiền đầy đủ cho toàn bộ 47 trường hợp đã xác nhận, không chờ khách
report. Support sẽ ưu tiên xử lý ngay các ticket tương tự phát sinh thêm
trong lúc chờ fix.
Step 10: Tạo verification checklist sau khi fix
Mục tiêu: Chuẩn bị trước — ngay khi dev đưa fix lên staging/production, QA cần một checklist chạy ngay để xác nhận: (1) bug đã hết, (2) không có hành vi mới nào bị phá vỡ, (3) có cơ chế phát hiện sớm nếu vấn đề tương tự tái xuất trong tương lai.
Prompt:
Dựa trên toàn bộ bug report và root cause đã xác nhận, hãy tạo một
Post-Fix Verification Checklist cho QA chạy sau khi fix được deploy lên
staging rồi production. Chia thành 3 nhóm:
1. Functional verification — xác nhận bug cụ thể này đã hết.
2. Regression verification — xác nhận các luồng liên quan (theo phần
Regression Risk trong bug report) không bị phá vỡ.
3. Monitoring / early-detection — cơ chế phát hiện sớm nếu vấn đề tương tự
(double-charge dạng khác) xuất hiện trở lại trong tương lai, không phải
chờ khách report.
Mỗi item phải là hành động cụ thể, có thể tick pass/fail rõ ràng.
Output AI trả về:
## 1. Functional Verification
[ ] Chạy lại đúng Steps to Reproduce trong bug report (giả lập server-side
delay >8000ms) trên staging — xác nhận CHỈ 1 charge thành công, retry
dùng ĐÚNG idempotency_key ban đầu (kiểm tra trong log).
[ ] Chạy lại 5 lần với các mức delay khác nhau (8.1s, 10s, 15s, 30s) để
đảm bảo fix không phụ thuộc vào một giá trị timeout cụ thể.
[ ] Xác nhận: nếu request gốc thực sự FAIL ở server (không chỉ timeout ở
client), retry vẫn hoạt động bình thường và tạo đúng 1 charge thành công.
## 2. Regression Verification
[ ] Test luồng "user hủy checkout và bắt đầu lại từ đầu" (không phải retry
do timeout) — xác nhận lần checkout mới nhận idempotency_key HOÀN TOÀN
MỚI, không bị lẫn với key của lần trước.
[ ] Test toàn bộ luồng checkout happy-path trên cả web và mobile (Android,
iOS) — không có regression về UX hoặc thời gian phản hồi.
[ ] Rà soát mọi caller khác của sendChargeRequest() trong codebase (không
chỉ luồng checkout thông thường — kiểm tra cả subscription renewal,
partial refund flow nếu có dùng chung hàm) để đảm bảo không phụ thuộc
vào hành vi cũ.
[ ] Chạy lại bộ regression test suite hiện có của order-service +
payment-service, không chỉ test case mới viết cho bug này.
## 3. Monitoring / Early-Detection
[ ] Thiết lập alert/dashboard: cảnh báo khi phát hiện order_id có ≥2 row
payments với status='succeeded' trong vòng 24h (dùng lại chính query
từ Step 2, chạy định kỳ tự động, không chờ ai chủ động query tay).
[ ] Thêm metric theo dõi tỷ lệ client timeout khi gọi payment-service theo
platform — nếu tỷ lệ tăng bất thường, đó là tín hiệu sớm của điều
kiện (a) trong causal chain, cảnh báo trước khi có double-charge thật.
[ ] Xác nhận job đối soát (reconciliation) định kỳ với Stripe đã được thiết
lập, không chỉ chạy một lần cho 47 case ban đầu.
[ ] Sau 14 ngày kể từ khi fix lên production, chạy lại đúng query ở Step 2
cho khung thời gian SAU fix — xác nhận số case double-charge = 0.
The complete investigation output
Toàn bộ 10 step trên tạo ra một chuỗi tài liệu sống, mỗi tài liệu là input cho tài liệu kế tiếp — đây chính là sản phẩm cuối cùng của một buổi điều tra bug phức tạp bằng AI:
| Artifact | Tạo ở Step | Đối tượng đọc |
|---|---|---|
| Investigation Brief v1 | Step 1 | QA (bản thân, định hướng điều tra) |
| Query kết quả + Brief v2 | Step 2-3 | QA (thu hẹp hypothesis) |
| Log analysis + Contract analysis | Step 4-5 | QA + có thể dev tham khảo |
| Root cause statement + causal chain | Step 6 | QA, dùng làm nguồn cho báo cáo tiếp theo |
| Scope & impact assessment | Step 7 | QA + Product/Finance |
| Developer-grade bug report | Step 8 | Developer nhận fix |
| Stakeholder summary | Step 9 | Product Manager / Support Lead / leadership |
| Post-fix verification checklist | Step 10 | QA (bản thân, chạy lại khi có fix) |
Toàn bộ chuỗi này nên được lưu lại nguyên vẹn — không chỉ bug report cuối cùng — trong Jira ticket hoặc Confluence page liên kết, kèm chính các prompt đã dùng. Đây không phải thủ tục hình thức: khi có một bug "trông quen" trong tương lai (ví dụ một pattern double-something khác ở một service khác), việc có sẵn toàn bộ chain điều tra — không chỉ kết luận — giúp người điều tra sau tái sử dụng cả cách đặt câu hỏi, không chỉ câu trả lời. Một bug report tốt cho developer fix một lần; một investigation chain đầy đủ dạy cả team điều tra tốt hơn cho mọi lần sau.
Mẹo: Bug report do AI tạo ra luôn cần người nắm code (code owner) review lại phần "Suggested Fix" trước khi assign cho dev — AI thường đúng về HƯỚNG fix (ở đây: giữ key ổn định theo session) nhưng không biết convention, pattern, hay ràng buộc kiến trúc hiện có trong codebase. Đừng để dev nhận nguyên fix đó và implement mù; hãy để "Suggested Fix" là điểm khởi đầu cho thảo luận kỹ thuật, không phải chỉ thị cuối cùng.