level 12 / production-debugging

Root Cause Analysis

Apply structured root cause analysis to production failures using observability data.

RCA Overview

Root Cause Analysis (RCA) finds the underlying cause of an incident — not just the symptom. Test engineers contribute to RCA by:

  • Writing regression tests that prevent recurrence
  • Analysing test gaps that missed the bug
  • Verifying fixes with automated tests

The 5 Whys

Structured iterative questioning:

Incident: Checkout failing for 15% of users

Why 1: Why is checkout failing?
  → Payment service returning 503

Why 2: Why is payment service returning 503?
  → All DB connections exhausted

Why 3: Why are DB connections exhausted?
  → Connection pool not released after timeout

Why 4: Why is the pool not released?
  → Missing `finally { conn.release() }` in error path

Why 5: Why was this not caught in testing?
  → No test for partial payment failure + timeout scenario

Root cause: Error handling path skips connection release
Fix: Add `finally` block + add test for timeout error path

RCA Investigation Workflow

1. Detect     → Alert fires (Prometheus/Datadog)

2. Scope      → Which users? Which region? Since when?

3. Correlate  → Metrics spike? Deploy at same time? Config change?

4. Trace      → Find a failing request trace (traceId)

5. Logs       → Filter logs by traceId — find error + context

6. Reproduce  → Write a failing test that reproduces the issue

7. Fix        → Implement fix — test goes green

8. Post-mortem → Document timeline, root cause, prevention

Using Playwright to Reproduce Production Bugs

// RCA step 6: write a reproducing test from the incident
// Incident: users with expired trial accounts could complete checkout

test('RCA-2024-001: expired trial users are blocked at checkout', async ({ page, request }) => {
  // Setup: create user with expired trial
  const user = await request.post('/api/test/users', {
    data: { email: 'trial@test.com', trialExpiredAt: '2023-01-01' },
  });
  const { id: userId } = await user.json();

  // Reproduce: attempt checkout as expired trial user
  await loginAs(page, 'trial@test.com');
  await page.goto('/checkout');
  await page.fill('[name=card]', '4242424242424242');
  await page.click('[type=submit]');

  // Assert: should be blocked
  await expect(page.getByText('Your trial has expired')).toBeVisible();
  await expect(page).not.toHaveURL(/confirmation/);

  // Cleanup
  await request.delete(`/api/test/users/${userId}`);
});

Metrics-Driven Test Creation

When a metric alert fires, create a test that validates the SLO:

// Alert: p95 checkout latency > 2s. Add a test to catch regressions.
test('checkout p95 latency is within SLO', async ({ page }) => {
  const timings: number[] = [];

  for (let i = 0; i < 20; i++) {
    const start = Date.now();
    await page.goto('/checkout');
    await page.fill('[name=card]', '4242424242424242');
    await page.click('[type=submit]');
    await page.waitForURL(/confirmation/);
    timings.push(Date.now() - start);
    await page.goto('/');
  }

  timings.sort((a, b) => a - b);
  const p95 = timings[Math.floor(timings.length * 0.95)];
  expect(p95).toBeLessThan(2000);  // SLO: p95 < 2 seconds
});

Post-Mortem Template

## Incident: [Title]

**Date:** 2024-01-15
**Duration:** 14:20–15:45 UTC (85 minutes)
**Impact:** 15% of checkout requests failed

### Timeline
- 14:20 — Alert: error_rate > 5%
- 14:25 — On-call engineer acknowledges
- 14:40 — Root cause identified: missing finally block
- 15:30 — Fix deployed
- 15:45 — Error rate returns to baseline

### Root Cause
Connection pool exhaustion in payment service error path.
Missing `finally { conn.release() }` in timeout handler.

### Contributing Factors
- No test for timeout error path
- Connection pool size not monitored

### Action Items
- [ ] Add regression test for payment timeout path
- [ ] Add connection pool utilisation metric + alert
- [ ] Code review checklist: verify try/finally on DB connections

Test Gap Analysis

After each incident, assess the test gaps:

// Missing test matrix for the incident:
// ✅ Happy path: successful payment
// ✅ Invalid card: declined
// ❌ MISSING: payment service timeout
// ❌ MISSING: connection pool exhaustion simulation
// ❌ MISSING: retry after transient failure

// Add the missing tests after the incident
test('checkout retries once on payment service timeout', async ({ page }) => {
  await page.route('**/api/payment', async (route) => {
    const callCount = (route as any)._callCount ?? 0;
    (route as any)._callCount = callCount + 1;
    if (callCount === 0) {
      await new Promise(r => setTimeout(r, 5000));  // first call: timeout
    }
    await route.continue();  // second call: proceed
  });

  await page.goto('/checkout');
  await page.click('[type=submit]');
  await expect(page.getByText('Order confirmed')).toBeVisible({ timeout: 15000 });
});