Przejdź do głównej zawartości

Wzorce debugowania

Twój endpoint uwierzytelniania zawodzi przy mniej więcej 30% logowań na produkcji. Ślad stosu wskazuje na linię, która wygląda poprawnie, błąd znika w chwili, gdy podłączysz debugger, a twój PM chce szacowanego czasu naprawy. To rodzaj błędu, który pożera całe popołudnie. AI też nie zna magicznie odpowiedzi — ale użyte systematycznie zmienia frustrujące polowanie w zwartą pętlę: instrumentuj, odtwórz, skoreluj, napraw.

  • Prompt wielokrotnego użytku do instrumentowania sporadycznych błędów strategicznym logowaniem
  • Przepływ pracy zmieniający surowy produkcyjny ślad stosu w defensywną poprawkę
  • Gotowy prompt do napisania niezaliczonego testu warunku wyścigu, zanim naprawisz błąd
  • Przepis na śledzenie między usługami korelujący logi po ID żądania
  • Tryby awarii debugowania z pomocą AI — i jak utrzymać je w ryzach

Wzorzec o największej dźwigni: pozwól AI zinstrumentować podejrzaną ścieżkę, odtworzyć problem pod obciążeniem, a następnie przekaż logi z powrotem do korelacji. Przepływ pracy jest taki sam we wszystkich trzech narzędziach — zmienia się tylko powierzchnia (Cursor edytuje plik w edytorze, Claude Code działa headless w repozytorium, Codex działa w TUI lub Cloud).

  1. Opisz problem precyzyjnie. Niejasne dane wejściowe dają niejasne logowanie. Podaj objaw, częstotliwość i to, co już wykluczyłeś.

  2. Pozwól narzędziu dodać logowanie. Ten sam prompt napędza każde narzędzie:

    Otwórz auth.js, zaznacz funkcję validateToken i uruchom prompt w trybie Agent (Cmd/Ctrl+I). Przejrzyj różnicę inline przed akceptacją. Cursor dodaje ukierunkowane, strukturalne logowanie:

    async function validateToken(token) {
    console.log('[AUTH] validation started', {
    tokenLength: token?.length,
    at: Date.now(),
    });
    try {
    const decoded = jwt.verify(token, SECRET);
    const msToExpiry = decoded.exp * 1000 - Date.now();
    console.log('[AUTH] decoded', { userId: decoded.userId, msToExpiry });
    if (msToExpiry < 60_000) {
    console.warn('[AUTH] token expiring soon', { msToExpiry });
    }
    return decoded;
    } catch (error) {
    console.error('[AUTH] validation failed', {
    message: error.message,
    iat: jwt.decode(token)?.iat,
    });
    throw error;
    }
    }
  3. Odtwórz pod obciążeniem, aby ujawnić błąd zależny od czasu.

    Okno terminala
    # Bombarduj ścieżkę, aby sporadyczny błąd faktycznie wystąpił
    npm test -- --grep "authentication" --repeat 100 2>&1 | tee debug.log
  4. Przekaż logi z powrotem i poproś o korelację, nie o zgadywanie.

    Dobra odpowiedź zawęża zakres do testowalnej przyczyny — np. „błędy gromadzą się, gdy opóźnienie walidacji spycha msToExpiry na wartość ujemną; token wygasa w trakcie żądania. Wtórny sygnał: przesunięcie iat o ~4s między dwoma hostami”. Teraz naprawiasz tolerancję dryftu zegara i wyprzedzające odświeżanie w oparciu o dowody, a nie o przeczucie.

Wzorzec 2: Od produkcyjnego śladu stosu do defensywnej poprawki

Dział zatytułowany „Wzorzec 2: Od produkcyjnego śladu stosu do defensywnej poprawki”

Surowy ślad stosu z Sentry lub twoich logów to najbogatsze pojedyncze dane wejściowe, jakie możesz przekazać AI — przypina plik, linię i łańcuch wywołań. Zadanie polega na zmianie go w poprawkę, która obsługuje przypadek brzegowy bez zamiatania prawdziwej przyczyny pod dywan.

Rozróżnienie w tym ostatnim zdaniu to właśnie to, co powstrzymuje AI od wyciszenia prawdziwego błędu. Dobra odpowiedź rozdziela te dwa przypadki:

async processOrder(userId, orderData) {
// "Should never happen" -> fail loudly
if (!userId) throw new ValidationError('User ID required');
const user = await this.getUser(userId);
if (!user) throw new NotFoundError(`User ${userId} not found`);
// "Expected sometimes" -> degrade gracefully
if (!user.stripeCustomer?.id) {
logger.warn('User missing Stripe customer; creating one', { userId });
user.stripeCustomer = await this.createStripeCustomer(user);
}
return this.createOrder(user, orderData);
}

Gdy użycie sterty rośnie bez ograniczeń, zacznij od prawdziwych narzędzi, a potem pozwól AI zinterpretować dowody. Przechwyć zrzut wbudowanym inspektorem Node (node --inspect, następnie zakładka Memory w Chrome DevTools, lub node --heapsnapshot-signal=SIGUSR2), albo uruchom clinic.js (clinic heapprofiler) lub 0x dla wykresu płomieniowego. Przekaż AI rozbicie retained-size, a nie ogólnikowe „przecieka”.

Zwykłym winowajcą, którego ujawni AI, jest nieograniczona kolekcja — nasłuchiwacze, timery lub wpisy cache dodawane, ale nigdy nieusuwane. Poprawka to ścieżka czyszczenia, którą wywołujący faktycznie uruchamiają:

addListener(event, callback) {
const listener = { event, callback };
this.listeners.push(listener);
// Hand back an unsubscribe so callers can release the reference
return () => {
const i = this.listeners.indexOf(listener);
if (i > -1) this.listeners.splice(i, 1);
};
}

Identyfikuj i naprawiaj błędy związane z czasem. Najsilniejszym ruchem jest zmuszenie AI do napisania niezaliczonego testu, który odtwarza wyścig, zanim dotknie poprawki — inaczej nie odróżnisz, czy poprawka zadziałała.

// AI writes the failing test first
describe('Payment Processing Race Conditions', () => {
it('should handle concurrent submissions', async () => {
const userId = 'test-user';
const paymentData = { amount: 100, currency: 'USD' };
// Simulate rapid clicks
const promises = Array(5).fill(null).map(() =>
processPayment(userId, paymentData)
);
const results = await Promise.allSettled(promises);
// Only one should succeed
const successful = results.filter(r => r.status === 'fulfilled');
expect(successful).toHaveLength(1);
// Others should be rejected with idempotency error
const rejected = results.filter(r => r.status === 'rejected');
expect(rejected).toHaveLength(4);
rejected.forEach(r => {
expect(r.reason.message).toContain('Payment already processing');
});
});
});
// AI suggests idempotency solution
class PaymentService {
constructor() {
this.processingPayments = new Map();
}
async processPayment(userId, paymentData) {
const idempotencyKey = `${userId}-${Date.now()}`;
// Check if already processing
if (this.processingPayments.has(userId)) {
throw new ConflictError('Payment already processing');
}
// Mark as processing
this.processingPayments.set(userId, idempotencyKey);
try {
// Process payment
const result = await this.chargeCard(paymentData);
return result;
} finally {
// Always cleanup
this.processingPayments.delete(userId);
}
}
}

Gdy błąd obejmuje wiele usług, sygnał żyje w korelacji, a nie w żadnym pojedynczym pliku logu. Przepis: ostempluj ID żądania na brzegu, zbierz logi z każdego przeskoku i pozwól AI odtworzyć oś czasu.

Okno terminala
# Zbierz logi z każdego przeskoku (dostosuj do polecenia logów twojej platformy)
kubectl logs -l app=user-service --since=1h > user-service.log
kubectl logs -l app=payment-service --since=1h > payment-service.log
# Przekaż je do AI w celu odtworzenia osi czasu
claude "Correlate these logs by requestId and trace the failed payment flow for abc-123"

Odtworzona oś czasu zmienia „coś jest wolne” w „Payment Service był zablokowany 5s w oczekiwaniu na bazę danych, potem pula się wyczerpała i łańcuch wywołań kaskadowo padł” — przyczynę źródłową, na którą możesz zareagować. Dla bogatszych śladów podłącz spany OpenTelemetry zamiast parsować logi tekstowe; ten sam prompt korelacyjny działa na wyeksportowanych danych spanów.

Debugowanie z pomocą AI zawodzi w określony, rozpoznawalny sposób. Znajomość tych trybów odróżnia prawdziwe śledztwo od pewnie brzmiącej ślepej uliczki.