แก้ Loop จัดการ Error และรับมือ Incident
หาสาเหตุรากของ loop ด้วย 5 Whys, anti-loop pattern, profiling, error taxonomy, retry, circuit breaker, graceful shutdown และการรับมือ incident บน production
บทที่ 12 | แก้ Loop จัดการ Error และรับมือ Incident กลุ่มเป้าหมาย: Developer ทุกระดับที่ต้องการให้ระบบล้มแล้วฟื้นได้ และไม่ล้มซ้ำด้วยสาเหตุเดิม
การแก้ปัญหา Loop แบบลึก — Root Cause Analysis
บทที่ 4 สอน Protocol หยุด Loop บทนี้ไปลึกกว่านั้น โดยสอนวิธีวิเคราะห์ต้นเหตุ (Root Cause) อย่างเป็นระบบ และออกแบบโค้ดล่วงหน้าให้เกิด Loop น้อยที่สุด
Framework 5 Whys สำหรับ Loop Debugging
เทคนิค “5 Whys” จาก Toyota Production System ใช้ได้ผลดีมากกับการ debug ด้วย Claude ไม่จำเป็นต้องถาม “ทำไม” ครบ 5 ครั้งทุกครั้ง แค่ถามจนถึง root cause จริงๆ
ตัวอย่าง: แก้บักด้วย 5 Whys จริง
# สถานการณ์: Payment ล้มเหลวบ้างประปราย
> Why 1: ทำไม payment ถึง fail?
→ Stripe API return error "card_declined"
> Why 2: ทำไม card ถึงถูก decline?
→ Stripe บอกว่า insufficient_funds แต่ user บอกว่าเงินพอ
> Why 3: ทำไม Stripe ถึงเห็น insufficient_funds ทั้งที่เงินพอ?
→ ดู request payload พบว่า amount ที่ส่งไปเป็น 1000
แต่ควรเป็น 10.00 (หน่วยต่างกัน: บาท vs สตางค์)
> Why 4: ทำไม amount ถึงส่งผิดหน่วย?
→ Code ใน checkout.ts ไม่ได้ convert จาก บาท → สตางค์
ก่อนส่งให้ Stripe (Stripe ใช้หน่วยที่เล็กที่สุดของสกุลเงิน)
> Why 5: ทำไม conversion ถึงขาดไป?
→ Developer อ่าน doc ผิด คิดว่า Stripe ใช้หน่วยหลัก
และไม่มี unit test ที่ verify ค่า amount
# Root Cause: ขาด unit test + documentation ไม่ชัด
# Fix จริง: แก้ conversion + เพิ่ม test + เพิ่ม comment ใน CLAUDE.md
Prompt ให้ Claude ทำ 5 Whys
> ทำ 5 Whys Analysis สำหรับบักนี้:
[อธิบายอาการบัก]
ทำแบบนี้:
1. Why 1: อธิบาย symptom ที่เห็น
2. Why 2-5: ถามต่อเรื่อยๆ จาก answer ก่อนหน้า
3. หยุดเมื่อถึง root cause จริง (ไม่ต้องครบ 5 ถ้าเจอก่อน)
4. แยกระหว่าง "Fix Symptom" กับ "Fix Root Cause"
5. เสนอ solution สำหรับ root cause
6. เสนอวิธีป้องกันไม่ให้เกิดซ้ำ (systematic fix)
Debugging Playbook — คู่มือแก้บักแต่ละประเภท
แทนที่จะ describe ปัญหากว้างๆ ให้ระบุประเภทบักก่อน แล้วใช้ playbook ที่เหมาะสม:
| Playbook A: Logic Bug — ผลลัพธ์ผิด แต่ไม่มี Error |
|---|
ยากที่สุดในการ debug เพราะไม่มี error message ให้ follow Claude มักวน loop กับบักประเภทนี้
> Logic Bug Analysis:
สิ่งที่เห็น: [input] → [actual output]
สิ่งที่ต้องการ: [input] → [expected output]
ให้ทำดังนี้:
1. Trace execution path: อธิบายทีละ step ว่า code ทำอะไร
ตั้งแต่รับ input จนถึงส่ง output
2. หา divergence point: ตรงไหนที่ actual กับ expected เริ่มต่างกัน?
3. เพิ่ม assertion checks ตลอดทาง:
console.assert(value === expected, `Step N: got ${value}, want ${expected}`)
4. รัน และดูว่า assertion ล้มเหลวที่ step ไหน
อย่าแก้โค้ดจนกว่าจะ identify divergence point ได้
| Playbook B: Performance Bug — ช้าผิดปกติ |
|---|
> Performance Investigation:
อาการ: [ส่วนไหนช้า] ช้าประมาณ [X วินาที/ms]
Target: ต้องเร็วกว่า [Y วินาที/ms]
ทำตามลำดับนี้:
Phase 1 — Measure ก่อนแก้:
เพิ่ม timing ใน @[file]:
- console.time("total")
- console.time("db-query")
- console.time("data-transform")
- console.time("response")
แล้วรัน 3 ครั้ง บันทึกค่าเฉลี่ย
Phase 2 — Identify bottleneck:
step ไหนใช้เวลามากที่สุด?
Phase 3 — Optimize เฉพาะ bottleneck:
อย่า optimize ส่วนที่เร็วอยู่แล้ว
Phase 4 — Measure อีกครั้ง:
เปรียบเทียบก่อน/หลัง ต้อง [Y ms] ตาม target
| Playbook C: Memory Leak — RAM ค่อยๆ เพิ่มจนระบบช้า |
|---|
> Memory Leak Investigation:
อาการ: memory เพิ่มขึ้นเรื่อยๆ เมื่อใช้งานนาน
Step 1 — Confirm มี leak จริง:
เพิ่ม memory monitoring:
setInterval(() => {
const mem = process.memoryUsage();
console.log(`RSS: ${Math.round(mem.rss/1024/1024)}MB`,
`Heap: ${Math.round(mem.heapUsed/1024/1024)}MB`);
}, 5000);
Step 2 — หา pattern:
memory เพิ่มหลัง action ไหน?
ทุก request? หลัง upload? หลัง WebSocket connect?
Step 3 — วิเคราะห์ @[file] หา patterns เหล่านี้:
- Event listeners ที่ไม่ได้ removeEventListener
- setTimeout/setInterval ที่ไม่ได้ clearTimeout/clearInterval
- Global variables ที่เก็บ array/object ไว้ตลอด
- Cache ที่ไม่มี eviction policy
- Closure ที่ capture object ใหญ่
Step 4 — Fix และ verify ว่า memory คงที่หลังรัน 30 นาที
| Playbook D: Concurrency Bug — ผิดเฉพาะ high traffic |
|---|
> Concurrency Analysis:
อาการ: ปกติดี แต่พอมี concurrent users จะมีปัญหา
Step 1 — Reproduce locally:
สร้าง load test script ที่ส่ง concurrent requests:
for (let i = 0; i < 50; i++) {
promises.push(fetch("/api/[endpoint]"));
}
await Promise.all(promises);
Step 2 — วิเคราะห์ shared state:
หา variables ใน @[file] ที่:
- เป็น module-level (ไม่ใช่ request-level)
- ถูก read และ write ใน handler เดียวกัน
- ไม่มี locking/synchronization
Step 3 — เสนอ fix:
- Database-level locking: SELECT FOR UPDATE
- Optimistic locking: version field + retry
- Queue: serialize concurrent operations
- Idempotency key: ป้องกัน duplicate processing
อธิบาย tradeoffs ของแต่ละ option ก่อนเลือก
Anti-Loop Patterns — ออกแบบให้ Loop เกิดน้อยลง
วิธีที่ดีที่สุดในการหลีกเลี่ยง Loop คือออกแบบโค้ดและ prompt ให้ดีตั้งแต่ต้น:
Pattern 1: Contract-First Development
กำหนด Interface และ Contract ชัดเจนก่อน implement ทำให้ Claude มี specification ที่ชัดเจน ไม่ต้องเดา:
# แทนที่จะบอก "สร้าง payment service"
# ให้เขียน contract ก่อน แล้วให้ Claude implement
> นี่คือ Contract ที่ต้องการ:
// Input
interface ProcessPaymentInput {
userId: string;
amount: number; // หน่วย: บาท (เช่น 99.50)
currency: "THB" | "USD";
description: string;
idempotencyKey: string; // ป้องกัน duplicate charge
}
// Success Output
interface ProcessPaymentSuccess {
success: true;
transactionId: string;
amount: number; // ยืนยัน amount ที่ charge จริง
timestamp: Date;
}
// Error Output
interface ProcessPaymentError {
success: false;
errorCode: "CARD_DECLINED" | "INSUFFICIENT_FUNDS" |
"INVALID_CARD" | "PROCESSING_ERROR";
message: string; // สำหรับ log (ไม่แสดง user)
userMessage: string; // สำหรับแสดง user (ภาษาไทย)
retryable: boolean; // บอก client ว่า retry ได้ไหม
}
implement processPayment(input: ProcessPaymentInput)
: Promise<ProcessPaymentSuccess | ProcessPaymentError>
ใช้ Stripe เป็น payment processor
convert amount จาก บาท → สตางค์ก่อนส่ง Stripe
Pattern 2: Error Budget Strategy
กำหนดล่วงหน้าว่า error ประเภทไหน Claude ควร retry เองได้ และประเภทไหนต้องหยุดแล้วถาม:
# เพิ่มใน CLAUDE.md
## 🔄 Error Handling Strategy
### Claude retry เองได้ (ไม่ต้องถาม):
- TypeScript type error → แก้ type ให้ถูกต้อง
- Import path ผิด → แก้ path
- Missing await → เพิ่ม await
- Unused variable → ลบออก
### Claude ต้องถามก่อน retry:
- Logic error (ผลลัพธ์ไม่ตรงที่คาด) → อธิบายความเข้าใจก่อน
- Database schema mismatch → แสดง schema ที่เห็น แล้วถาม
- External API error → แสดง error code + ถาม intent
### Claude ต้องหยุดและรายงาน:
- Security-related issue → อย่าแก้เองเด็ดขาด
- แก้เกิน 2 รอบแล้วยังไม่ได้ → สรุปสิ่งที่ลอง + ขอข้อมูลเพิ่ม
- ต้องแก้ไฟล์นอก scope → ขออนุมัติก่อน
Pattern 3: Checkpoint System
แบ่งงานเป็น checkpoint เล็กๆ และ verify แต่ละ checkpoint ก่อนไปต่อ ป้องกัน snowball effect ที่บักสะสม:
> สร้าง User Registration System ด้วย Checkpoint:
Checkpoint 1: Database schema
- สร้าง Prisma schema สำหรับ User
- รัน migration
- ✓ verify: npx prisma db pull แล้วดู schema
STOP → รอ approve ก่อนไป Checkpoint 2
Checkpoint 2: Validation layer
- สร้าง Zod schema สำหรับ registration input
- เขียน unit test ครอบคลุม valid/invalid cases
- ✓ verify: npm test ต้องผ่านทั้งหมด
STOP → รอ approve ก่อนไป Checkpoint 3
Checkpoint 3: Service layer
- สร้าง registerUser() function
- hash password, check duplicate email
- ✓ verify: integration test กับ test database
STOP → รอ approve ก่อนไป Checkpoint 4
Checkpoint 4: API Route
- สร้าง POST /api/auth/register
- ✓ verify: curl test ทุก case
ถ้า checkpoint ไหนล้มเหลว → หยุด ไม่ไป checkpoint ถัดไป
การใช้ Claude Debug Claude
เทคนิคที่ทรงพลังมาก: ให้ Claude ตัวใหม่วิเคราะห์ปัญหาโดยไม่รู้ว่า Claude ตัวไหนเขียนโค้ดนั้น
# Session 1: Claude A สร้าง code และเจอบัก
# Claude A แก้ไม่ได้ใน 2 รอบ
# เปิด Session ใหม่ (Claude B)
> [Fresh session - ไม่มี context เดิม]
ผมได้รับ code นี้มาและมันมีบัก:
[วาง code ทั้งหมด]
อาการ: [อธิบาย]
Error: [copy error]
วิเคราะห์ code นี้จากมุมมองภายนอก:
1. สิ่งที่ code นี้พยายามทำคืออะไร?
2. เห็น design issue อะไรไหม?
3. บัคน่าจะอยู่ที่ไหน?
4. ถ้าคุณเขียน code นี้ใหม่จาก scratch
จะเปลี่ยน approach อะไร?
# Claude B มักเห็นปัญหาที่ Claude A มองข้ามไป
# เพราะไม่มี cognitive bias จากการเขียนโค้ดนั้นเอง
💡 เมื่อไหร่ควรใช้ Claude Debug Claude
ใช้เมื่อ: Claude วน loop แก้ไม่ได้เกิน 2 รอบ
ใช้เมื่อ: โค้ดซับซ้อนมากจนอธิบายยาก
ใช้เมื่อ: ต้องการ second opinion ก่อน merge
ไม่ต้องใช้: บักง่ายๆ หรือแก้ได้ในรอบแรก
Production Quality — ตั้งแต่เริ่มต้นจนถึง Live
Production code ไม่ได้หมายความแค่ “รันได้” แต่หมายถึงโค้ดที่ reliable, maintainable, observable และ recoverable ส่วนนี้จะไล่ทุกขั้นตอนตั้งแต่ design จนถึงผู้ใช้งานจริง
ขั้นตอนที่ 1 — Requirements & Design
งานส่วนใหญ่ล้มเหลวที่ขั้นตอนนี้ ไม่ใช่ที่ implementation ให้ Claude ช่วย clarify requirements ก่อนเสมอ
# Prompt สำหรับ clarify requirements
> ก่อน implement ขอ clarify requirements:
Feature ที่ต้องการ: [อธิบาย]
ช่วยถามคำถามที่จำเป็นก่อน implement:
1. Happy path: ขั้นตอนปกติเป็นอย่างไร?
2. Edge cases: กรณีพิเศษที่ต้อง handle?
3. Error cases: เกิดอะไรขึ้นถ้า X ล้มเหลว?
4. Performance: ต้องรองรับ concurrent users กี่คน?
5. Security: ใครเข้าถึงได้? Data sensitivity ระดับไหน?
6. Rollback: ถ้า feature นี้พัง rollback ยังไง?
หลังจาก clarify แล้ว สรุป acceptance criteria
ที่ใช้ verify ว่า feature เสร็จและถูกต้อง
Design Document ที่ Claude ช่วยสร้าง
> สร้าง Design Document สำหรับ [Feature] ประกอบด้วย:
## Summary
อธิบาย feature ใน 2-3 ประโยค
## Architecture
- Data flow diagram (text format)
- Components ที่เกี่ยวข้อง
- External dependencies
## API Contract
- Endpoints ใหม่ (input/output types)
- Error responses
## Database Changes
- Tables/columns ที่เพิ่ม/แก้
- Migration strategy
- Rollback plan
## Testing Strategy
- Unit tests ที่ต้องเขียน
- Integration tests
- E2E scenarios
## Risks & Mitigations
- ความเสี่ยงที่เห็น
- วิธีลด risk
## Acceptance Criteria
✓ [criterion 1]
✓ [criterion 2]
ขั้นตอนที่ 2 — Implementation with Quality Gates
แต่ละ phase ของ implementation มี Quality Gate ที่ต้องผ่านก่อนไปต่อ Claude ช่วย verify ได้ทุก gate
| Phase | งานที่ทำ | Quality Gate | Claude ช่วยได้ |
|---|---|---|---|
| Data Layer | Schema, migrations, indexes | Migration รันได้, rollback ได้ | เขียน migration + test |
| Business Logic | Core functions, validation | Unit tests ผ่าน 100% | เขียน tests + implement |
| API Layer | Routes, middleware, auth | Integration tests ผ่าน | เขียน tests + routes |
| UI Layer | Components, forms, states | E2E tests ผ่าน key flows | เขียน Playwright tests |
| Performance | Query optimization, caching | Load test ผ่าน target | วิเคราะห์ bottleneck |
| Security | Auth check, input validation | Security checklist ผ่าน | รัน OWASP checklist |
ขั้นตอนที่ 3 — Testing Strategy แบบสมบูรณ์
Testing คือส่วนที่ Vibe Coder มักข้ามมากที่สุด แต่เป็นส่วนที่ทำให้ production code แตกต่างจาก prototype
| Unit Testing ด้วย Vitest + Claude |
|---|
# ให้ Claude เขียน comprehensive unit tests
> เขียน unit tests สำหรับ @src/lib/pricing.ts
ครอบคลุม ALL cases:
1. Happy paths (input ถูกต้อง → output ที่คาด)
2. Edge cases:
- จำนวนที่ boundary: 0, negative, MAX_SAFE_INTEGER
- null/undefined inputs
- empty arrays/objects
3. Error cases:
- invalid input type
- business rule violations
4. Precision cases:
- floating point (0.1 + 0.2 ≠ 0.3)
- currency rounding
ใช้ Vitest, organize เป็น describe blocks
ชื่อ test ต้อง readable: "should return X when Y"
# ตัวอย่าง output ที่ดี
describe("calculateDiscount", () => {
describe("percentage discount", () => {
it("should apply 10% discount to 1000", () => {
expect(calculateDiscount(1000, { type: "percent", value: 10 }))
.toBe(900);
});
it("should handle 0% discount", () => {...});
it("should cap at 100% discount", () => {...});
});
describe("fixed discount", () => {...});
describe("edge cases", () => {...});
});
| Integration Testing ด้วย Supertest |
|---|
> เขียน integration tests สำหรับ POST /api/orders
ใช้ Supertest + test database
Test cases:
1. ✅ Success: create order ถูกต้อง → 201 + order object
2. ❌ Unauthorized: ไม่มี token → 401
3. ❌ Validation: ข้อมูลไม่ครบ → 400 + error details
4. ❌ Business Rule: สินค้าหมด stock → 422 + user message
5. ⚡ Idempotency: ส่ง request เดิม 2 ครั้ง → สร้าง order เดียว
6. 🔒 Authorization: user ดู order ของคนอื่น → 403
Setup:
- เริ่ม test database ที่สะอาดก่อนแต่ละ test
- ล้าง database หลังแต่ละ test
- mock Stripe API (ไม่ call real API ใน test)
# ตัวอย่าง
describe("POST /api/orders", () => {
beforeEach(async () => {
await db.cleanDatabase();
await db.seed.basicProducts();
});
it("should create order and return 201", async () => {
const res = await request(app)
.post("/api/orders")
.set("Authorization", `Bearer ${testToken}`)
.send({ items: [{ productId: "p1", quantity: 2 }] });
expect(res.status).toBe(201);
expect(res.body.id).toBeDefined();
expect(res.body.total).toBe(200);
});
});
| E2E Testing ด้วย Playwright |
|---|
> เขียน E2E tests สำหรับ checkout flow
ใช้ Playwright + staging environment
Critical flows ที่ต้อง test:
1. Happy path: เลือกสินค้า → checkout → payment → confirmation
2. Failed payment: กรอก card ผิด → error message → retry
3. Out of stock: สินค้าหมดระหว่าง checkout → error + redirect
4. Session timeout: ค้างนาน → session หมด → redirect to login
# ตัวอย่าง Playwright test
test("complete checkout flow", async ({ page }) => {
await page.goto("/products");
// เลือกสินค้า
await page.click("[data-testid=product-1]");
await page.click("[data-testid=add-to-cart]");
// ไป checkout
await page.click("[data-testid=cart-icon]");
await page.click("[data-testid=checkout-btn]");
// กรอก payment
await page.fill("[data-testid=card-number]", "4242424242424242");
await page.fill("[data-testid=card-expiry]", "12/25");
await page.fill("[data-testid=card-cvc]", "123");
await page.click("[data-testid=pay-btn]");
// verify confirmation
await expect(page.locator("[data-testid=order-confirmed]"))
.toBeVisible({ timeout: 10000 });
});
ขั้นตอนที่ 4 — Security Checklist
Security เป็นสิ่งที่ Vibe Coder มักลืมมากที่สุด ใช้ checklist นี้ทุกครั้งก่อน deploy:
# ให้ Claude รัน Security Audit
> รัน Security Audit สำหรับ code ที่เพิ่งเขียน
ตรวจสอบ OWASP Top 10 ที่เกี่ยวข้อง:
□ Injection:
- SQL: ใช้ parameterized queries ทั้งหมด?
- NoSQL: sanitize MongoDB operators?
- Command: exec() หรือ shell command ใช้ input โดยตรงไหม?
□ Authentication:
- ทุก route ที่ sensitive มี auth check?
- JWT validation ถูกต้อง (verify signature, expiry)?
- Rate limiting บน login endpoint?
□ Authorization (IDOR):
- User ดูแก้ข้อมูลตัวเองเท่านั้น?
- ตรวจสอบ ownership ก่อน return data?
□ Sensitive Data:
- Password hashed ด้วย bcrypt (cost >= 10)?
- ไม่ return sensitive fields (password, token)?
- Log ไม่มี PII หรือ credentials?
□ Input Validation:
- Validate และ sanitize ทุก input?
- File upload: check type, size, scan content?
- URL parameters: validate format?
□ Error Handling:
- Error messages ไม่ leak internal info?
- Stack traces ไม่ถูกส่งไป client?
สำหรับแต่ละรายการ: ✅ ผ่าน / ❌ พบปัญหา (อธิบาย)
ขั้นตอนที่ 5 — Performance Testing และ Benchmarking
ก่อน deploy ต้องรู้ว่าโค้ดรองรับ load ได้มากแค่ไหน และ bottleneck อยู่ที่ไหน
| Load Testing ด้วย k6 |
|---|
# ให้ Claude เขียน k6 load test script
> เขียน k6 load test สำหรับ API ที่สำคัญ
Target: /api/products/search
Expected: p95 latency < 500ms ที่ 100 concurrent users
// k6 script ที่ Claude จะสร้าง
import http from "k6/http";
import { check, sleep } from "k6";
export const options = {
stages: [
{ duration: "30s", target: 10 }, // warm up
{ duration: "1m", target: 100 }, // ramp to target
{ duration: "3m", target: 100 }, // steady state
{ duration: "30s", target: 0 }, // ramp down
],
thresholds: {
http_req_duration: ["p(95)<500"], // 95% ต้อง < 500ms
http_req_failed: ["rate<0.01"], // error rate < 1%
},
};
export default function() {
const params = {
headers: { Authorization: `Bearer ${__ENV.TEST_TOKEN}` },
};
const searchTerms = ["shirt", "pants", "shoes", "hat"];
const term = searchTerms[Math.floor(Math.random() * searchTerms.length)];
const res = http.get(`${__ENV.BASE_URL}/api/products/search?q=${term}`, params);
check(res, {
"status 200": (r) => r.status === 200,
"has products": (r) => JSON.parse(r.body).products.length >= 0,
"response time OK": (r) => r.timings.duration < 500,
});
sleep(1);
}
# รัน: k6 run -e BASE_URL=https://staging.myapp.com \
# -e TEST_TOKEN=xxx script.js
| Database Query Performance ด้วย EXPLAIN ANALYZE |
|---|
> วิเคราะห์ performance ของ query นี้:
[แปะ Prisma query]
สร้าง raw SQL EXPLAIN ANALYZE version
เพื่อดู execution plan:
1. Seq Scan หรือ Index Scan?
2. estimated vs actual rows ต่างกันมากไหม?
3. สิ้นเปลือง memory ที่ step ไหน?
จากนั้นเสนอ:
- Index ที่ควรเพิ่ม
- Query rewrite ถ้าจำเป็น
- Caching strategy ถ้าเหมาะสม
# ตัวอย่าง EXPLAIN ANALYZE output ที่ Claude วิเคราะห์
EXPLAIN ANALYZE
SELECT u.*, COUNT(o.id) as order_count
FROM users u
LEFT JOIN orders o ON o.user_id = u.id
WHERE u.created_at > NOW() - INTERVAL "30 days"
GROUP BY u.id;
# Claude จะเห็น Seq Scan บน users และเสนอ:
# CREATE INDEX idx_users_created_at ON users(created_at DESC);
ขั้นตอนที่ 6 — Observability: Logging, Metrics, Tracing
โค้ดที่ดีต้องสังเกตได้ เมื่อเกิดปัญหาใน production ต้องหาสาเหตุได้ภายในนาที ไม่ใช่ชั่วโมง
| Structured Logging |
|---|
> เพิ่ม structured logging ใน @src/lib/logger.ts
Requirements:
- ใช้ pino (เร็วกว่า winston 10x)
- Log format: JSON (เพื่อ parse ได้)
- Log levels: error, warn, info, debug
- ทุก log ต้องมี: timestamp, level, service,
requestId, userId (ถ้ามี), message, data
- ห้าม log: passwords, tokens, credit card
- Production: log ระดับ info ขึ้นไปเท่านั้น
# ตัวอย่าง logger ที่ดี
logger.info({
event: "payment.processed",
requestId: ctx.requestId,
userId: user.id,
orderId: order.id,
amount: order.total,
duration: timer.elapsed(),
// ไม่มี card number!
}, "Payment processed successfully");
> เพิ่ม request ID middleware ที่ inject requestId
ใน every request context เพื่อ trace across services
| Health Check Endpoint |
|---|
> สร้าง /api/health endpoint สำหรับ monitoring
ต้องตรวจสอบ:
□ Database: ping ได้? query ง่ายๆ รันได้?
□ Redis (ถ้าใช้): ping ได้?
□ External APIs: Stripe, SendGrid reachable?
□ Disk space: เหลือ > 20%?
□ Memory: heap usage < 80%?
Response format:
{
"status": "healthy" | "degraded" | "unhealthy",
"timestamp": "2025-01-15T10:00:00Z",
"version": "1.4.2",
"checks": {
"database": { "status": "ok", "latency": 5 },
"redis": { "status": "ok", "latency": 1 },
"stripe": { "status": "ok", "latency": 120 }
}
}
- 200: healthy หรือ degraded (บาง service ช้าแต่ยังทำงานได้)
- 503: unhealthy (core service ล้มเหลว)
- Timeout ของแต่ละ check: 2 วินาที
ขั้นตอนที่ 7 — Deployment Strategy
การ deploy ที่ดีต้องมีแผน rollback ชัดเจน และ minimize downtime
| Zero-downtime Deployment Checklist |
|---|
> สร้าง deployment checklist สำหรับ [feature]
โดย Claude ช่วยระบุ:
## Pre-deployment
□ Tests ทั้งหมดผ่าน (unit, integration, e2e)
□ Database migration reviewed (backward compatible?)
□ Environment variables ใหม่ set ใน production?
□ Feature flags configured?
□ Monitoring alerts set up?
## Migration Strategy
(สำคัญ: migration ต้อง backward compatible อย่างน้อย 1 version)
Step 1: Deploy migration (เพิ่ม column ใหม่, nullable)
Step 2: Deploy app (ใช้ column ใหม่)
Step 3: Backfill data เก่า (background job)
Step 4: Make column NOT NULL (next release)
## Deployment Steps
1. Deploy to staging → smoke test
2. Deploy to production (canary: 5% traffic)
3. Monitor 15 นาที (error rate, latency)
4. Expand to 50% → monitor
5. Full rollout → monitor 1 ชั่วโมง
## Rollback Plan
Trigger: error rate > 1% หรือ p95 > 2s
Time to rollback: < 5 นาที
Command: [ระบุคำสั่ง rollback จริง]
QA Testing — การทดสอบก่อน Production
QA (Quality Assurance) คือกระบวนการตรวจสอบว่า feature ทำงานถูกต้องในทุกสถานการณ์ Claude ช่วยออกแบบและรัน QA ได้อย่างเป็นระบบ
QA Test Plan ด้วย Claude
> สร้าง QA Test Plan สำหรับ [Feature Name]
ข้อมูล feature:
[อธิบาย feature ที่สร้าง]
สร้าง test plan ที่ครอบคลุม:
## Functional Testing
- Happy path scenarios (ทุก user journey)
- Negative scenarios (input ผิด, permission ไม่พอ)
- Boundary testing (min/max values)
## Browser/Device Compatibility
- Chrome, Firefox, Safari (latest)
- Mobile: iOS Safari, Android Chrome
- Responsive: 320px, 768px, 1280px, 1920px
## Accessibility Testing
- Keyboard navigation ครบ?
- Screen reader compatible?
- Color contrast WCAG 2.1 AA?
- Focus indicators visible?
## Data Integrity
- ข้อมูลถูก save ถูกต้อง?
- ข้อมูลแสดงถูกต้องหลัง refresh?
- Concurrent users: race condition?
## Format: Test Case Table
| ID | Scenario | Steps | Expected | Priority |
Automated QA Checklist สำหรับ Vibe Coding
ใช้ checklist นี้ทุกครั้งก่อน merge PR:
# .claude/commands/qa-check.md
# QA Pre-merge Checklist
# รัน automated checks ทั้งหมดและรายงานผล
1. รัน: npm run typecheck
✓ ต้องไม่มี TypeScript errors
2. รัน: npm run lint
✓ ต้องไม่มี ESLint errors (warnings OK)
3. รัน: npm test
✓ ต้องผ่าน 100% (0 failures)
✓ coverage ต้อง >= 70% สำหรับ new code
4. รัน: npm run build
✓ ต้อง build สำเร็จ ไม่มี errors
5. รัน Security check:
npm audit --audit-level=high
✓ ต้องไม่มี high/critical vulnerabilities
6. ตรวจสอบ git diff --stat
✓ ไม่มี .env ถูก commit
✓ ไม่มี console.log ที่ไม่จำเป็น
✓ ไม่มี TODO ที่ should not be in prod
7. ทดสอบ manual บน localhost:
- Happy path ที่สำคัญที่สุด
- Error case ที่น่าจะเกิดบ่อย
รายงานผลเป็น: ✅ ผ่าน / ❌ ล้มเหลว (พร้อมรายละเอียด)
Visual Regression Testing
ตรวจสอบว่า UI ไม่เปลี่ยนโดยไม่ตั้งใจ หลังจาก refactor หรือ upgrade dependency:
# ติดตั้ง Playwright screenshot testing
npm install @playwright/test
> เขียน visual regression tests สำหรับหน้าสำคัญ:
- Homepage
- Product listing
- Checkout flow (3 steps)
- User profile
// visual.test.ts
import { test, expect } from "@playwright/test";
test("homepage visual", async ({ page }) => {
await page.goto("/");
await page.waitForLoadState("networkidle");
await expect(page).toHaveScreenshot("homepage.png", {
maxDiffPixels: 100, // อนุญาต pixel ต่างได้ 100 pixels
});
});
# ครั้งแรก: รันเพื่อสร้าง baseline
npx playwright test visual --update-snapshots
# ครั้งต่อไป: เปรียบเทียบกับ baseline
npx playwright test visual
# ถ้า UI เปลี่ยน → test fail → ต้อง approve หรือ fix
A/B Testing และ Feature Flags
A/B Testing คือการทดสอบ feature สองแบบพร้อมกันกับผู้ใช้จริง เพื่อวัดว่าแบบไหนดีกว่า Feature Flags ช่วย control ว่าใครเห็น feature ไหน
Feature Flags — Control การ Release
Feature Flags ช่วยให้ deploy code ก่อน แล้วค่อย enable สำหรับผู้ใช้ทีหลัง ลด risk ของการ deploy
# ให้ Claude implement Feature Flag system
> สร้าง Feature Flag system สำหรับโปรเจค
Requirements:
- Store flags ใน database (เปลี่ยนได้ไม่ต้อง redeploy)
- Target: ทุก user / user ที่ระบุ / percentage
- Admin UI: เปิด/ปิด flag ได้ทันที
- Cache: 1 นาที (ไม่ hit DB ทุก request)
Schema:
model FeatureFlag {
id String @id @default(cuid())
key String @unique // "new-checkout"
enabled Boolean @default(false)
rolloutPct Int @default(0) // 0-100%
userIds String[] // specific users
description String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
Usage ใน code:
if (await featureFlag.isEnabled("new-checkout", user.id)) {
// show new checkout
} else {
// show old checkout
}
A/B Test Framework
A/B Test วัดว่า variant ไหนให้ conversion rate ที่ดีกว่า
> สร้าง A/B Test framework ที่:
1. Assign user ไปยัง variant อย่าง consistent
(user เดิม → variant เดิมเสมอ)
2. Track events ที่สำคัญ (conversion goal)
3. Calculate statistical significance
4. Dashboard แสดงผลลัพธ์
ตัวอย่าง A/B Test: CTA Button
Variant A (control): "เพิ่มลงตะกร้า" (สีน้ำเงิน)
Variant B (treatment): "ซื้อเลย" (สีแดง)
Goal: Click rate
Duration: 2 สัปดาห์ หรือ n=1000/variant
// ab-test.ts
export async function getVariant(
testId: string,
userId: string
): Promise<"control" | "treatment"> {
// deterministic hash เพื่อให้ consistent
const hash = crypto
.createHash("sha256")
.update(`${testId}:${userId}`)
.digest("hex");
const num = parseInt(hash.substring(0, 8), 16);
return num % 2 === 0 ? "control" : "treatment";
}
export async function trackEvent(
testId: string,
userId: string,
event: "impression" | "conversion"
) {
const variant = await getVariant(testId, userId);
await db.abTestEvent.create({
data: { testId, userId, variant, event }
});
}
วิเคราะห์ผล A/B Test ด้วย Claude
# หลังเก็บ data ได้ 2 สัปดาห์
> วิเคราะห์ผล A/B Test "cta-button-color"
Data:
Control: 1,024 impressions, 87 conversions (8.5%)
Treatment: 1,031 impressions, 112 conversions (10.9%)
ช่วย:
1. คำนวณ statistical significance (p-value)
ใช้ Chi-square test หรือ Fisher exact test
2. คำนวณ confidence interval ของความต่าง
3. บอกว่าผลมี significance ไหม (p < 0.05)?
4. ถ้า significant: recommend ทำอะไร?
5. ถ้าไม่ significant: ต้องการ sample size เท่าไหร่?
Benchmark และ Profiling
Benchmark คือการวัดประสิทธิภาพอย่างเป็นระบบ Profiling คือการหาว่าโค้ดส่วนไหนช้า เป็นทักษะสำคัญที่ทำให้ production code ทำงานได้เร็วและประหยัด resource
Micro-benchmark ด้วย Vitest
> เขียน benchmark เพื่อเปรียบเทียบ algorithm A กับ B:
Algorithm A: ใช้ Array.filter().map()
Algorithm B: ใช้ single Array.reduce()
Test scenarios:
- Array ขนาด 100, 1,000, 10,000, 100,000 elements
- รันแต่ละ scenario 1000 ครั้ง
- รายงาน: mean, median, p99, memory usage
// benchmark.test.ts
import { bench, describe } from "vitest";
describe("search algorithms", () => {
const data = Array.from({ length: 10000 },
(_, i) => ({ id: i, value: Math.random() })
);
bench("filter + map", () => {
data.filter(x => x.value > 0.5).map(x => x.id);
});
bench("reduce", () => {
data.reduce((acc, x) => {
if (x.value > 0.5) acc.push(x.id);
return acc;
}, [] as number[]);
});
});
# รัน: npx vitest bench
Node.js Profiling ด้วย —prof
> ช่วยตั้งค่า profiling สำหรับ Node.js app
เพื่อหา CPU bottleneck
# รัน app พร้อม profiler
node --prof dist/server.js
# ส่ง traffic (load test)
k6 run profile-test.js
# หยุด app แล้ว process profile
node --prof-process isolate-*.log > profile.txt
# ให้ Claude วิเคราะห์
> นี่คือ Node.js CPU profile:
[วาง top functions จาก profile.txt]
วิเคราะห์:
1. Function ไหนใช้ CPU มากที่สุด?
2. เป็น bottleneck จริงหรือ false positive?
3. วิธี optimize ที่เหมาะสม?
# Alternative: ใช้ clinic.js (ง่ายกว่า)
npm install -g clinic
clinic doctor -- node dist/server.js
# เปิด browser อัตโนมัติ แสดง flame graph
Database Query Profiling
> สร้าง database profiling setup:
1. เปิด query logging ใน Prisma:
const prisma = new PrismaClient({
log: [
{ level: "query", emit: "event" },
{ level: "error", emit: "stdout" },
],
});
prisma.$on("query", (e) => {
if (e.duration > 100) { // log queries > 100ms
logger.warn({
query: e.query,
params: e.params,
duration: e.duration,
}, "Slow query detected");
}
});
2. เพิ่ม pg_stat_statements ใน PostgreSQL
เพื่อ track query statistics:
CREATE EXTENSION pg_stat_statements;
-- ดู slow queries
SELECT query, calls, mean_exec_time, total_exec_time
FROM pg_stat_statements
WHERE mean_exec_time > 100
ORDER BY total_exec_time DESC
LIMIT 10;
Error Handling และ Recovery Strategy
Production system ต้องรับมือกับ failure ได้อย่างสง่างาม ไม่ใช่แค่ “อย่าให้เกิด error” แต่ต้องรู้ว่าจะทำอะไรเมื่อเกิดขึ้น
Error Taxonomy — จัดประเภท Error อย่างถูกต้อง
| ประเภท Error | ตัวอย่าง | วิธี Handle | Retry? |
|---|---|---|---|
| Transient | Network timeout, DB connection blip | Retry with backoff | ได้ |
| Validation | Email format ผิด, field ขาด | Return 400 พร้อม detail | ไม่ได้ |
| Business Rule | Coupon หมดอายุ, สินค้าหมด stock | Return 422 พร้อม user message | ไม่ได้ |
| Auth | Token หมดอายุ, ไม่มีสิทธิ์ | Return 401/403 | บางครั้ง |
| Dependency | Stripe down, SendGrid fail | Fallback หรือ queue | ได้ |
| System | OOM, disk full, bug ใน code | Alert ทีม, log ละเอียด | บางครั้ง |
Retry Pattern ด้วย Exponential Backoff
> สร้าง retry utility ที่ใช้ได้ทั่วโปรเจค
// retry.ts
export async function withRetry<T>(
fn: () => Promise<T>,
options: {
maxAttempts?: number; // default: 3
initialDelay?: number; // default: 1000ms
maxDelay?: number; // default: 30000ms
backoffFactor?: number; // default: 2 (exponential)
retryIf?: (error: Error) => boolean;
onRetry?: (attempt: number, error: Error) => void;
} = {}
): Promise<T> {
const {
maxAttempts = 3,
initialDelay = 1000,
maxDelay = 30000,
backoffFactor = 2,
retryIf = () => true,
onRetry = () => {},
} = options;
let lastError: Error;
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
return await fn();
} catch (error) {
lastError = error as Error;
if (attempt === maxAttempts || !retryIf(lastError)) {
throw lastError;
}
const delay = Math.min(
initialDelay * Math.pow(backoffFactor, attempt - 1),
maxDelay
);
const jitter = delay * 0.1 * Math.random(); // ±10% jitter
onRetry(attempt, lastError);
await new Promise(resolve => setTimeout(resolve, delay + jitter));
}
}
throw lastError!;
}
// Usage
const result = await withRetry(
() => stripe.paymentIntents.create(params),
{
maxAttempts: 3,
retryIf: (err) => err.message.includes("network"),
onRetry: (attempt, err) => logger.warn({ attempt, err }, "Retrying payment"),
}
);
Circuit Breaker Pattern
ป้องกันไม่ให้ระบบถาม external service ที่กำลัง fail ซ้ำๆ ช่วยให้ fail fast และ recover ได้เร็ว
> implement Circuit Breaker สำหรับ external API calls
States:
- CLOSED: ทำงานปกติ
- OPEN: หยุดส่ง request (fail fast)
- HALF_OPEN: ลอง request 1 อัน ถ้าสำเร็จ → CLOSED
Config:
- failureThreshold: 5 failures → OPEN
- successThreshold: 2 successes → CLOSED (from HALF_OPEN)
- timeout: 30 วินาที ก่อน HALF_OPEN
// ใช้ opossum library
npm install opossum
import CircuitBreaker from "opossum";
const stripeBreaker = new CircuitBreaker(
(params) => stripe.paymentIntents.create(params),
{
timeout: 5000, // 5s timeout per request
errorThresholdPercentage: 50, // 50% errors → OPEN
resetTimeout: 30000, // 30s ก่อน HALF_OPEN
volumeThreshold: 10, // ต้องมี 10+ requests ก่อน open
}
);
stripeBreaker.on("open", () => logger.error("Circuit OPEN: Stripe"));
stripeBreaker.on("halfOpen", () => logger.warn("Circuit HALF_OPEN: Testing Stripe"));
stripeBreaker.on("close", () => logger.info("Circuit CLOSED: Stripe recovered"));
// Usage (transparent to caller)
const intent = await stripeBreaker.fire(params);
Graceful Shutdown
เมื่อ server ต้อง restart ต้องทำให้ request ที่กำลังประมวลผลเสร็จก่อน ไม่ทิ้งงานกลางคัน
> implement Graceful Shutdown สำหรับ Express server
Requirements:
- รับ SIGTERM signal
- หยุดรับ request ใหม่
- รอ request ที่กำลังทำอยู่ให้เสร็จ (timeout: 30s)
- ปิด database connection
- Exit ด้วย code 0
// shutdown.ts
let isShuttingDown = false;
export function gracefulShutdown(server: Server) {
process.on("SIGTERM", async () => {
logger.info("SIGTERM received, starting graceful shutdown");
isShuttingDown = true;
server.close(async () => {
try {
await prisma.$disconnect();
await redis.quit();
logger.info("Graceful shutdown complete");
process.exit(0);
} catch (err) {
logger.error(err, "Error during shutdown");
process.exit(1);
}
});
// Force shutdown หลัง 30s
setTimeout(() => {
logger.error("Forced shutdown after timeout");
process.exit(1);
}, 30000);
});
}
// middleware: reject new requests during shutdown
app.use((req, res, next) => {
if (isShuttingDown) {
res.set("Connection", "close");
return res.status(503).json({ error: "Server shutting down" });
}
next();
});
Production Incident Management
เมื่อเกิดปัญหาใน production วิธีที่ตอบสนองมีผลอย่างมากต่อ downtime ที่เกิดขึ้น Claude ช่วยทั้งในการ debug และ communicate ได้
Incident Response Playbook
| 1 | Detect — รู้ว่ามีปัญหา Alert จาก monitoring ที่ตั้งไว้ หรือ user report มาก Claude Code ช่วย query log ได้ทันที |
|---|
# ตรวจสอบสถานะเร็ว
> ช่วย query log ของ 15 นาทีที่ผ่านมา:
รัน: grep "ERROR" /var/log/app/app.log | tail -50
หรือ query Cloudwatch:
aws logs filter-log-events --log-group-name /app/production
--start-time $(date -d "15 minutes ago" +%s000)
--filter-pattern "ERROR"
สรุปให้ว่า:
1. Error ประเภทไหนเกิดมากที่สุด?
2. เริ่มตั้งแต่เวลาไหน?
3. endpoint ไหนได้รับผลกระทบ?
| 2 | Triage — ประเมิน severity บอก Claude ข้อมูลที่มี Claude ช่วย assess impact และ priority |
|---|
> Incident Assessment:
ข้อมูลที่มี:
- Error rate: [X%] (ปกติ < 0.1%)
- Affected users: [N] คน
- Affected features: [list]
- Duration: [X minutes]
ประเมิน:
1. Severity: P1 (ระบบล่ม) / P2 (feature สำคัญพัง) / P3 (ผลกระทบน้อย)
2. ผู้ที่ต้องแจ้ง: team lead, CTO, users?
3. Timeline สำหรับ fix: ชั่วโมง / วัน
4. Quick mitigation ที่ทำได้ทันที?
| 3 | Mitigate — ลดผลกระทบก่อน ทำ workaround เพื่อลด downtime ก่อน แล้วค่อยหา root cause |
|---|
# วิธี mitigate ที่ทำได้เร็ว
> เราพบว่า payment feature พัง
ยังไม่รู้สาเหตุ
เสนอ mitigation options ที่ทำได้ใน 5 นาที:
- Feature flag: ปิด payment ชั่วคราว
- Maintenance mode: redirect ไปหน้าแจ้ง
- Rollback: ย้อนกลับ version ก่อนหน้า
- Scale up: เพิ่ม server ถ้า load issue
ระบุ command จริงสำหรับแต่ละ option
| 4 | Root Cause Analysis เมื่อ mitigate แล้ว ค่อยหา root cause อย่างเป็นระบบ |
|---|
| 5 | Post-mortem หลังแก้แล้ว ทำ post-mortem เพื่อป้องกันไม่ให้เกิดซ้ำ |
|---|
> เขียน Post-mortem document สำหรับ incident นี้:
ข้อมูล:
- เริ่ม: [เวลา]
- แก้ได้: [เวลา]
- Duration: [X minutes]
- Impact: [N users, N transactions affected]
Template:
## Timeline
[เวลา] อะไรเกิดขึ้น
## Root Cause
อธิบาย root cause จริงๆ (ไม่ใช่ symptom)
## Contributing Factors
สิ่งที่ทำให้ปัญหาแย่ลงหรือแก้ช้า
## What Went Well
สิ่งที่ทำได้ดีในการ respond
## Action Items
| Action | Owner | Due Date | Priority |
|--------|-------|----------|----------|
| เพิ่ม test ที่ catch bug นี้ | @john | 2025-01-20 | P1 |
| เพิ่ม alert สำหรับ error pattern นี้ | @jane | 2025-01-18 | P1 |
## Prevention
จะป้องกันไม่ให้เกิดซ้ำด้วยวิธีไหน?
สรุป: Production-Ready Checklist
รวม checklist ทั้งหมดไว้ใช้ก่อน deploy production จริง:
# .claude/commands/prod-ready.md
# Production Readiness Checklist
## Code Quality
□ TypeScript strict mode: ไม่มี any
□ Tests: unit + integration + e2e ผ่าน
□ Test coverage: >= 70% สำหรับ new code
□ Linting: ไม่มี errors
□ Build: สำเร็จ ไม่มี warnings สำคัญ
## Security
□ Authentication: ทุก protected route มี auth check
□ Authorization: ตรวจสอบ ownership ก่อน return data
□ Input validation: ทุก input ผ่าน validation
□ SQL: ไม่มี raw string concatenation
□ Secrets: ไม่มี hardcoded credentials
□ Dependencies: npm audit ไม่มี high/critical
## Performance
□ Database: indexes ครบสำหรับ queries ที่ใช้บ่อย
□ N+1: ไม่มี N+1 query ใน critical paths
□ Load test: ผ่าน target performance
□ Caching: ใช้ cache สำหรับ expensive operations
## Observability
□ Logging: structured logs ครบ
□ Error tracking: Sentry configured
□ Health check: /api/health ทำงาน
□ Alerts: monitoring alerts set up
## Reliability
□ Error handling: ทุก async มี try/catch
□ Retry logic: transient errors มี retry
□ Graceful shutdown: รองรับ SIGTERM
□ Circuit breaker: external APIs มี protection
## Deployment
□ Migration: backward compatible
□ Rollback: plan ชัดเจน ทดสอบแล้ว
□ Feature flags: configured
□ Runbook: อัปเดตแล้ว
□ ทุกข้อ: ✅ พร้อม deploy | ❌ ต้องแก้ก่อน
คำศัพท์ประจำบท
| 5 Whys | เทคนิคหา Root Cause โดยถาม “ทำไม” ซ้ำๆ จนถึงต้นเหตุจริง |
|---|---|
| Root Cause | สาเหตุที่แท้จริงของปัญหา ไม่ใช่ symptom ที่เห็น |
| Contract-First | กำหนด Interface/API contract ก่อน implement เพื่อลด ambiguity |
| Circuit Breaker | Pattern ที่หยุดส่ง request ไป service ที่กำลัง fail เพื่อ fail fast |
| Exponential Backoff | วิธี retry โดยรอนานขึ้นทุกครั้ง เพื่อไม่กด service ที่กำลัง recover |
| Graceful Shutdown | การ shutdown ที่รอ request ปัจจุบันเสร็จก่อน ไม่ทิ้งงานกลางคัน |
| Feature Flag | Switch ที่เปิด/ปิด feature ได้โดยไม่ต้อง redeploy |
| A/B Test | การทดสอบ 2 variants พร้อมกันกับ users จริง เพื่อวัดว่าแบบไหนดีกว่า |
| Statistical Significance | ระดับความมั่นใจทางสถิติว่าผลต่างไม่ได้เกิดจากความบังเอิญ |
| p-value | ค่าที่บอกว่าผลที่เห็นเกิดจากความบังเอิญน้อยแค่ไหน (< 0.05 = significant) |
| Benchmark | การวัดประสิทธิภาพอย่างเป็นระบบเพื่อเปรียบเทียบ |
| Profiling | การหาว่าโค้ดส่วนไหนใช้ CPU/Memory มากที่สุด |
| Flame Graph | กราฟแสดง call stack ที่ใช้ CPU จนหา bottleneck ได้ |
| EXPLAIN ANALYZE | คำสั่ง SQL ที่แสดง execution plan จริงของ query |
| Load Test | การทดสอบระบบภายใต้ load จำนวนมาก เพื่อหา breaking point |
| Canary Deployment | การ deploy ไปยัง % เล็กๆ ของ users ก่อน แล้วค่อยขยาย |
| Post-mortem | การวิเคราะห์หลัง incident เพื่อหาสาเหตุและป้องกันซ้ำ |
| MTTR | Mean Time To Recovery — เวลาเฉลี่ยในการแก้ incident |
| Error Budget | ปริมาณ error/downtime ที่ยอมรับได้ใน SLA (เช่น 0.1%/เดือน) |