Processus de debugging systématique une fois la cause identifiée. MANDATORY pour bug fixing...
Appliquer un processus de debugging systématique pour corriger un bug de manière robuste et éviter sa réapparition.
Debugging ≠ Quick Fix
Un bon debugging :
Objectif: Documenter précisément le bug avant toute intervention.
Template:
## Bug Definition
**Comportement Observé**:
[Description précise du bug]
**Environnement**:
- OS: [macOS, Linux, Windows]
- Browser: [Chrome, Safari, Firefox]
- Version: [Backend version, Frontend version]
- Database: [PostgreSQL version]
**Reproduction Steps**:
1. [Action 1]
2. [Action 2]
3. [Action 3]
→ Bug occurs
**Expected Behavior**:
[Ce qui devrait se passer]
**Actual Behavior**:
[Ce qui se passe réellement]
**Impact**:
- Severity: [Critical, High, Medium, Low]
- User Impact: [Bloquant, Gênant, Mineur]
- Business Impact: [Description]
Exemple:
## Bug Definition
**Comportement Observé**:
Lors de la création d'un club, le formulaire se soumet mais aucun club n'est créé en base de données.
**Environnement**:
- OS: macOS 14
- Browser: Chrome 120
- Backend: v1.2.0
- Database: PostgreSQL 15
**Reproduction Steps**:
1. Se connecter en tant que Coach
2. Aller sur /signup/coach/club
3. Remplir le formulaire (nom, description)
4. Cliquer sur "Créer le club"
→ Bug: Requête retourne 201 mais aucune donnée en DB
**Expected Behavior**:
Club créé en base de données et redirection vers dashboard
**Actual Behavior**:
201 Created retourné, mais SELECT sur clubs table retourne 0 résultats
**Impact**:
- Severity: Critical
- User Impact: Bloquant (impossible de créer un club)
- Business Impact: Onboarding cassé pour tous les nouveaux coaches
Objectif: S'assurer de pouvoir reproduire le bug de manière fiable.
Actions:
Test Reproductible:
// tests/e2e/club-creation.spec.ts
describe('Club Creation Bug', () => {
it('should create club in database when form is submitted', async () => {
// Arrange
const clubData = {
name: 'Test Club',
description: 'Test Description',
};
// Act
const response = await request(app)
.post('/api/clubs')
.set('Authorization', `Bearer ${validToken}`)
.send(clubData)
.expect(201);
// Assert - Vérifier que le club existe en DB
const clubInDb = await prisma.club.findUnique({
where: { id: response.body.id },
});
expect(clubInDb).toBeDefined();
expect(clubInDb.name).toBe(clubData.name);
});
});
Objectif: Identifier les causes les plus probables (déjà fait avec bug-finder).
Template:
## Root Causes (Prioritized)
### Cause #1: [Description] - Likelihood: 80%
**Hypothesis**: [Explication de pourquoi]
**Evidence**: [Preuves observées]
**Location**: [Fichier(s) + ligne(s)]
### Cause #2: [Description] - Likelihood: 50%
**Hypothesis**: [...]
**Evidence**: [...]
**Location**: [...]
### Cause #3: [Description] - Likelihood: 20%
**Hypothesis**: [...]
**Evidence**: [...]
**Location**: [...]
Objectif: Décider dans quel ordre investiguer les causes.
Matrice de Priorisation:
Impact / Likelihood | High (80%+) | Medium (50-80%) | Low (<50%)
-----------------------|-------------|-----------------|------------
Critical (Bloquant) | P0 | P1 | P2
High (Gênant) | P1 | P2 | P3
Medium (Mineur) | P2 | P3 | P3
Ordre d'Investigation: P0 → P1 → P2 → P3
Exemple:
## Investigation Priority
1. **P0**: Cause #1 (Transaction non committée) - Critical + 80% likelihood
2. **P1**: Cause #2 (Mapper retourne undefined) - Critical + 50% likelihood
3. **P3**: Cause #3 (Race condition) - Critical + 20% likelihood
**Plan**: Investiguer Cause #1 en premier.
Objectif: Ajouter des logs stratégiques pour observer le comportement réel.
Où Logger:
Pattern de Logs:
// ✅ BON - Logs structurés avec contexte
export class CreateClubHandler {
async execute(command: CreateClubCommand): Promise<string> {
console.log('[CreateClubHandler] START', {
commandId: command.id,
clubName: command.name,
userId: command.userId,
});
try {
// Log avant création entity
console.log('[CreateClubHandler] Creating domain entity');
const club = Club.create(command.name, command.description, command.userId);
console.log('[CreateClubHandler] Domain entity created', { clubId: club.getId() });
// Log avant save
console.log('[CreateClubHandler] Saving to repository');
const savedClub = await this.clubRepository.create(club);
console.log('[CreateClubHandler] Saved successfully', {
clubId: savedClub.getId(),
clubName: savedClub.getName().getValue(),
});
// Log après save
console.log('[CreateClubHandler] Verifying save...');
const verification = await this.clubRepository.findById(savedClub.getId());
console.log('[CreateClubHandler] Verification result', {
found: !!verification,
id: verification?.getId(),
});
return savedClub.getId();
} catch (error) {
console.error('[CreateClubHandler] ERROR', {
error: error.message,
stack: error.stack,
command,
});
throw error;
}
}
}
CRITICAL: Supprimer tous les logs de debug avant de commit.
Objectif: Utiliser la technique de binary search pour isoler le problème.
Méthode:
Exemple:
## Binary Search Process
**Flow Complet**:
Controller → Handler → Entity Creation → Repository → Mapper → Prisma → DB
**Iteration 1**: Test au milieu (Repository)
- Log avant: clubRepository.create()
- Log après: clubRepository.create()
→ Result: Log "avant" OK, Log "après" OK
→ Conclusion: Problème APRÈS repository (Mapper ou Prisma ou DB)
**Iteration 2**: Test Mapper
- Log avant: ClubMapper.toPrisma()
- Log après: ClubMapper.toPrisma()
→ Result: Log "avant" OK, Log "après" retourne undefined
→ Conclusion: Problème dans ClubMapper.toPrisma()
**Iteration 3**: Inspect ClubMapper.toPrisma()
- Line 42: return undefined si club.getSubscription() est null
→ ROOT CAUSE FOUND: Mapper ne gère pas le cas subscription null
Objectif: Créer un test reproductible avant de fixer.
Pattern:
// tests/unit/infrastructure/persistence/mappers/club.mapper.spec.ts
describe('ClubMapper', () => {
describe('toPrisma()', () => {
it('should handle club without subscription', () => {
// Arrange
const club = new Club(
'club-123',
ClubName.create('Test Club'),
'Description',
'user-123',
null // Subscription is null
);
// Act
const prismaData = ClubMapper.toPrisma(club);
// Assert
expect(prismaData).toBeDefined();
expect(prismaData.name).toBe('Test Club');
expect(prismaData.subscriptionId).toBeNull(); // Should handle null subscription
});
});
});
Vérifier que le test FAIL:
yarn test club.mapper.spec.ts
# Output:
# FAIL ClubMapper › toPrisma() › should handle club without subscription
# Expected: defined
# Received: undefined
Objectif: Appliquer le fix le plus simple et le plus ciblé possible.
Principes:
Exemple de Fix:
// infrastructure/persistence/mappers/club.mapper.ts
export class ClubMapper {
static toPrisma(club: Club): Prisma.ClubCreateInput {
// ❌ AVANT (bug)
return {
id: club.getId(),
name: club.getName().getValue(),
description: club.getDescription(),
subscriptionId: club.getSubscription().getId(), // Crash si null
userId: club.getUserId(),
};
// ✅ APRÈS (fix minimal)
return {
id: club.getId(),
name: club.getName().getValue(),
description: club.getDescription(),
subscriptionId: club.getSubscription()?.getId() ?? null, // Handle null
userId: club.getUserId(),
};
}
}
Objectif: S'assurer que le fix ne casse rien d'autre.
Actions:
Commandes:
# Test spécifique
yarn test club.mapper.spec.ts
# ✅ PASS ClubMapper › toPrisma() › should handle club without subscription
# Full test suite
yarn test
# Coverage
yarn test:cov
Objectif: Empêcher le bug de réapparaître à l'avenir.
Types de Protection:
Exemples:
Protection 1 - Tests:
// Tests complets pour edge cases
describe('ClubMapper', () => {
it('should handle club without subscription', () => { /* ... */ });
it('should handle club with subscription', () => { /* ... */ });
it('should handle club with null description', () => { /* ... */ });
it('should throw if club ID is missing', () => { /* ... */ });
});
Protection 2 - Type Guard:
// domain/entities/club.entity.ts
export class Club {
getSubscription(): Subscription | null {
return this.subscription;
}
// Guard method
hasSubscription(): boolean {
return this.subscription !== null;
}
// Usage in mapper
static toPrisma(club: Club): Prisma.ClubCreateInput {
return {
// ...
subscriptionId: club.hasSubscription()
? club.getSubscription()!.getId()
: null,
};
}
}
Protection 3 - Validation:
// application/commands/create-club/create-club.handler.ts
export class CreateClubHandler {
async execute(command: CreateClubCommand): Promise<string> {
// Validation explicite
if (!command.userId) {
throw new ValidationException('User ID is required');
}
if (!command.name || command.name.trim().length === 0) {
throw new ValidationException('Club name cannot be empty');
}
// ... reste du code
}
}
Protection 4 - Documentation:
// infrastructure/persistence/mappers/club.mapper.ts
export class ClubMapper {
/**
* Converts a domain Club entity to Prisma format.
*
* IMPORTANT: This mapper handles clubs without subscriptions.
* When a club is first created, it may not have a subscription yet.
* The subscriptionId field will be null in this case.
*
* @param club - Domain Club entity
* @returns Prisma ClubCreateInput
*/
static toPrisma(club: Club): Prisma.ClubCreateInput {
return {
id: club.getId(),
name: club.getName().getValue(),
description: club.getDescription(),
subscriptionId: club.getSubscription()?.getId() ?? null,
userId: club.getUserId(),
};
}
}
Objectif: Documenter pour l'équipe et pour l'avenir.
Template:
## Root Cause Analysis
**Bug**: [Description courte]
**Root Cause**: [Cause racine identifiée]
**Why It Happened**:
[Explication de pourquoi le bug s'est produit]
**Fix Applied**:
[Description du fix]
**Prevention Measures**:
1. [Test ajouté]
2. [Guard ajouté]
3. [Validation ajoutée]
4. [Documentation ajoutée]
**Lessons Learned**:
[Ce qu'on a appris pour éviter ce type de bug à l'avenir]
Exemple:
## Root Cause Analysis
**Bug**: Club creation returns 201 but club not saved to database
**Root Cause**: ClubMapper.toPrisma() was calling `club.getSubscription().getId()` without null check. When a club is created without a subscription, this throws an error that was silently caught somewhere in the stack, preventing the save.
**Why It Happened**:
- Initial implementation assumed all clubs have subscriptions
- No test coverage for the edge case of club without subscription
- Error was caught silently by NestJS exception filter
**Fix Applied**:
Changed `club.getSubscription().getId()` to `club.getSubscription()?.getId() ?? null` to handle null subscriptions gracefully.
**Prevention Measures**:
1. Added unit test: "should handle club without subscription"
2. Added type guard: Club.hasSubscription() method
3. Added validation in CreateClubHandler to ensure subscriptionId is explicitly set
4. Added JSDoc comment explaining the null case
**Lessons Learned**:
- Always handle nullable relations in mappers
- Add tests for edge cases during initial implementation
- Use optional chaining for potentially null properties
❌ MAUVAIS
Dev: "Je pense que c'est ça, je change le code"
✅ BON
Dev: "Je reproduis d'abord le bug localement"
Dev: "J'écris un test qui fail"
Dev: "ALORS je fixe"
// ❌ MAUVAIS - Fix trop large
function createClub(data) {
// Refactor complet de toute la fonction
// Ajout de 5 nouvelles fonctionnalités
// Changement de l'architecture
// ...
}
// ✅ BON - Fix minimal et ciblé
function createClub(data) {
// Une seule ligne changée
const subscriptionId = data.subscription?.id ?? null;
// ...
}
❌ MAUVAIS
Dev fixe le bug, commit, termine
✅ BON
Dev fixe le bug
Dev ajoute test pour empêcher régression
Dev commit avec test
❌ MAUVAIS
yarn test club.mapper.spec.ts
# ✅ Pass
git commit -m "Fix club creation"
✅ BON
yarn test club.mapper.spec.ts
# ✅ Pass
yarn test
# ✅ All pass
git commit -m "Fix club creation"
Rappel CRITIQUE : Toujours reproduire → tester → fixer → protéger → documenter.