Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
136 changes: 136 additions & 0 deletions .claude/skills/sentry-issues/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
---
name: sentry-issues
description: Analyse les dernières issues Sentry du projet, examine les stack traces et propose des corrections dans le code source.
argument-hint: "[nombre d'issues (défaut: 10)] [--assign] [--comment <PR_URL>] [--fix]"
allowed-tools:
- Bash
- Read
- Edit
- Grep
- Glob
- Agent
---

# Analyse des issues Sentry

Tu es un agent spécialisé dans l'analyse des erreurs Sentry pour le projet **Code du travail numérique**.

## Configuration

- **Sentry URL** : `https://sentry2.fabrique.social.gouv.fr`
- **Organisation** : `incubateur`
- **Projet** : `fabnum-code-du-travail-numerique`
- **Token** : utilise la variable d'environnement `SENTRY_AUTH_TOKEN`. Si elle n'est pas définie, **demande le token à l'utilisateur** avant de continuer. Ne jamais le lire depuis un fichier de config.
- **Frontend** : `packages/code-du-travail-frontend/`

## Arguments

- `$ARGUMENTS` peut contenir :
- Un nombre d'issues à récupérer (défaut : 10)
- `--assign` : assigner les issues à l'utilisateur courant après analyse
- `--comment <PR_URL>` : ajouter un commentaire sur chaque issue avec le lien vers la PR
- `--fix` : proposer et appliquer des corrections dans le code source

## Étapes

### 1. Récupérer le token

Vérifie si la variable d'environnement `SENTRY_AUTH_TOKEN` est définie :

```bash
echo "${SENTRY_AUTH_TOKEN:-NOT_SET}"
```

- Si elle est définie, utilise-la pour tous les appels API.
- **Si elle n'est PAS définie** (`NOT_SET`), demande à l'utilisateur de fournir son token Sentry (Auth Token commençant par `sntryu_`). Attends sa réponse avant de continuer. Ne tente PAS de le lire depuis un fichier.

### 2. Valider le projet

Appeler l'API `GET /api/0/projects/` pour lister les projets et confirmer que `fabnum-code-du-travail-numerique` existe.

### 3. Récupérer les N dernières issues non résolues

```
GET /api/0/projects/incubateur/fabnum-code-du-travail-numerique/issues/?query=is:unresolved&sort=date&limit={N}
```

Pour chaque issue, afficher : ID, titre, culprit (route), niveau, nombre d'occurrences, première/dernière apparition.

### 4. Récupérer les stack traces

Pour chaque issue pertinente (ignorer les titres minifiés sauf si on peut récupérer le message original) :

```
GET /api/0/issues/{issue_id}/events/latest/
```

Extraire :
- Le type et message de l'exception
- Les frames de la stack trace (en priorité ceux marqués `inApp: true`)
- Les breadcrumbs d'erreur
- Les tags (URL, browser, OS)

### 5. Analyser et catégoriser

Classer chaque issue dans une catégorie :
- **Hydration mismatch** : chercher `Math.random()`, `generateUUID()`, `Date.now()`, `typeof window`, `usePathname()` sans garde dans le code source
- **ChunkLoadError** : erreur de cache après redéploiement
- **404 bruyantes** : `Sentry.captureMessage` ou `captureException` appelés pour des pages non trouvées
- **Erreurs externes** : scripts tiers (iframes, CDN) en échec
- **Bugs applicatifs** : vraies erreurs dans le code métier (destructuration, null pointer, etc.)
- **Code obsolète en cache** : erreurs sur du code qui n'existe plus dans le source actuel

### 6. Proposer des corrections (si `--fix`)

Pour chaque issue identifiée comme corrigeable :
1. Localiser le fichier source correspondant via `Grep` et `Glob`
2. Lire le fichier pour comprendre le contexte
3. Proposer une correction avec `Edit`
4. Expliquer le raisonnement

Corrections types :
- Remplacer `Math.random()` / `uuid()` dans le rendu SSR par des IDs déterministes
- Supprimer les `captureMessage`/`captureException` pour les 404
- Ajouter un auto-reload sur `ChunkLoadError` dans le error boundary
- Ajouter des gardes null/undefined avant destructuration
- Améliorer le contexte des `captureException` (ajouter `extra`)

### 7. Commenter les issues (si `--comment <PR_URL>`)

Pour chaque issue, ajouter un commentaire via :

```
POST /api/0/issues/{issue_id}/notes/
{"text": "Fix en cours dans la PR <PR_URL>\n\nCause : <explication>\nFix : <description du fix>\n\n-> A passer en fixed après merge et release."}
```

### 8. Assigner les issues (si `--assign`)

Récupérer l'ID utilisateur courant via les notes existantes sur une issue :

```
GET /api/0/issues/{issue_id}/notes/
```

Puis assigner :

```
PUT /api/0/issues/{issue_id}/
{"assignedTo": "user:{user_id}"}
```

### 9. Produire un rapport

Afficher un tableau récapitulatif :

| # | Issue | Occurrences | Catégorie | Action | Statut |
|---|-------|------------|-----------|--------|--------|

## Bonnes pratiques

- Ne jamais hardcoder le token dans le code ou les fichiers de config du projet
- Si `SENTRY_AUTH_TOKEN` n'est pas défini, toujours demander le token à l'utilisateur
- Privilégier `jq` pour parser le JSON (plus concis et lisible). Si `jq` n'est pas disponible, utiliser `python3` en fallback
- Toujours vérifier le code HTTP des réponses API
- Ne pas créer de commit automatiquement, laisser l'utilisateur décider
- Lancer les recherches de code en parallèle avec des agents `Explore` pour la performance
10 changes: 10 additions & 0 deletions packages/code-du-travail-frontend/app/error.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,20 @@

export default function Error({
error,
reset,

Check warning on line 16 in packages/code-du-travail-frontend/app/error.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove the unused function parameter "reset" or rename it to "_reset" to make intention explicit.

See more on https://sonarcloud.io/project/issues?id=SocialGouv_code-du-travail-numerique&issues=AZ1tfFxK6XxdP0HxUYSZ&open=AZ1tfFxK6XxdP0HxUYSZ&pullRequest=7225
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
useEffect(() => {
if (error.name === "ChunkLoadError") {
const key = `chunk-reload:${error.message}`;
if (!sessionStorage.getItem(key)) {
sessionStorage.setItem(key, "1");
window.location.reload();
return;
}
}
Comment thread
m-maillot marked this conversation as resolved.
Comment on lines +22 to +29
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

C'est spécifique comme fix cela ?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ha oui très spécifique.
Pour corriger le ChunkLoadError il propose un reload. Mais revu-bot a indiqué que ça pouvait faire une boucle infini (#7225 (comment)) et du coup il a proposé ce fix pour palier à ce problème :)

console.error(error);
Sentry.captureException(error);
}, [error]);
Expand Down
3 changes: 0 additions & 3 deletions packages/code-du-travail-frontend/app/not-found.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

import { NotFound } from "../src/modules/errors/NotFound";
import { DsfrLayout } from "../src/modules/layout";
import * as Sentry from "@sentry/nextjs";
import { Metadata } from "next";

export const metadata: Metadata = {
Expand All @@ -11,8 +10,6 @@ export const metadata: Metadata = {
};

export default function Index() {
Sentry.captureMessage("Page non trouvée");
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Les 404 n'ont pas vocation a être récupéré dans Sentry. D'autant plus qu'on ne sait pas les analyser tellement il y en a.


return (
<DsfrLayout>
<NotFound />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import parse, {
} from "html-react-parser";
import React, { ElementType, JSX } from "react";
import { AccordionWithAnchor } from "./AccordionWithAnchor";
import { v4 as generateUUID } from "uuid";

import { fr } from "@codegouvfr/react-dsfr";
import Link from "./Link";
import { slugify } from "@socialgouv/cdtn-utils";
Expand Down Expand Up @@ -40,19 +40,23 @@ const mapItem = (params: Options, domNode: Element, summary: Element) => ({
trim: true,
}),
});
const mapToAccordion = (titleLevel: numberLevel, isParent: boolean, items) => {
const mapToAccordion = (
titleLevel: numberLevel,
isParent: boolean,
items: any[]
) => {
const props = titleLevel <= 6 ? { titleLevel } : {};

return (
<div className={fr.cx("fr-my-3w")}>
<AccordionWithAnchor
{...props}
data-testid="contrib-accordion"
items={items.map((item) => ({
items={items.map((item, index) => ({
...item,
...(isParent
? { id: slugify(item.title) }
: { id: slugify(item.title) + "_" + generateUUID() }),
: { id: slugify(item.title) + "_" + index }),
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ca c'est à vérifier ! Car on avait pas mal de soucis sur les accordéons ! Je vais passer sur les écrans qui posaient soucis.

}))}
titleAs={`h${titleLevel}`}
/>
Expand Down Expand Up @@ -329,7 +333,7 @@ const options = (params: Options): HTMLReactParserOptions => {
titleAs={`h${titleLevel}`}
items={[
{
id: `infographic-description-${Math.random().toString(36).substring(2, 15)}`,
id: `infographic-description-${infoId}`,
title: "Lire la description",
content: (
<>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,18 +53,18 @@ exports[`DisplayContent Accordions should not fail if no summary tag 1`] = `
class="fr-accordion__title"
>
<button
aria-controls="conges-payes_1233-collapse"
aria-controls="conges-payes_0-collapse"
aria-expanded="false"
class="fr-accordion__btn"
id="conges-payes_1233__toggle-btn"
id="conges-payes_0__toggle-btn"
type="button"
>
Congés payés
</button>
</h4>
<div
class="fr-collapse"
id="conges-payes_1233-collapse"
id="conges-payes_0-collapse"
>
<div>
<div
Expand All @@ -81,10 +81,10 @@ exports[`DisplayContent Accordions should not fail if no summary tag 1`] = `
class="fr-accordion__title"
>
<button
aria-controls="dates-des-conges-fixees-avant-la-notification-du-licenciement_1230-collapse"
aria-controls="dates-des-conges-fixees-avant-la-notification-du-licenciement_0-collapse"
aria-expanded="false"
class="fr-accordion__btn"
id="dates-des-conges-fixees-avant-la-notification-du-licenciement_1230__toggle-btn"
id="dates-des-conges-fixees-avant-la-notification-du-licenciement_0__toggle-btn"
type="button"
>

Expand All @@ -94,7 +94,7 @@ exports[`DisplayContent Accordions should not fail if no summary tag 1`] = `
</h5>
<div
class="fr-collapse"
id="dates-des-conges-fixees-avant-la-notification-du-licenciement_1230-collapse"
id="dates-des-conges-fixees-avant-la-notification-du-licenciement_0-collapse"
>
<div>
<p
Expand All @@ -117,10 +117,10 @@ exports[`DisplayContent Accordions should not fail if no summary tag 1`] = `
class="fr-accordion__title"
>
<button
aria-controls="dates-des-conges-fixees-apres-la-notification-du-licenciement_1231-collapse"
aria-controls="dates-des-conges-fixees-apres-la-notification-du-licenciement_1-collapse"
aria-expanded="false"
class="fr-accordion__btn"
id="dates-des-conges-fixees-apres-la-notification-du-licenciement_1231__toggle-btn"
id="dates-des-conges-fixees-apres-la-notification-du-licenciement_1__toggle-btn"
type="button"
>

Expand All @@ -130,7 +130,7 @@ exports[`DisplayContent Accordions should not fail if no summary tag 1`] = `
</h5>
<div
class="fr-collapse"
id="dates-des-conges-fixees-apres-la-notification-du-licenciement_1231-collapse"
id="dates-des-conges-fixees-apres-la-notification-du-licenciement_1-collapse"
>
<div>
<p
Expand All @@ -154,18 +154,18 @@ exports[`DisplayContent Accordions should not fail if no summary tag 1`] = `
class="fr-accordion__title"
>
<button
aria-controls="licenciement-notifie-pendant-les-conges-payes_1232-collapse"
aria-controls="licenciement-notifie-pendant-les-conges-payes_2-collapse"
aria-expanded="false"
class="fr-accordion__btn"
id="licenciement-notifie-pendant-les-conges-payes_1232__toggle-btn"
id="licenciement-notifie-pendant-les-conges-payes_2__toggle-btn"
type="button"
>
Licenciement notifié pendant les congés payés
</button>
</h5>
<div
class="fr-collapse"
id="licenciement-notifie-pendant-les-conges-payes_1232-collapse"
id="licenciement-notifie-pendant-les-conges-payes_2-collapse"
>
<div>
<p
Expand Down Expand Up @@ -278,18 +278,18 @@ exports[`DisplayContent Accordions should replace details element within details
class="fr-accordion__title"
>
<button
aria-controls="ceci-est-un-sous-titre_1234-collapse"
aria-controls="ceci-est-un-sous-titre_0-collapse"
aria-expanded="false"
class="fr-accordion__btn"
id="ceci-est-un-sous-titre_1234__toggle-btn"
id="ceci-est-un-sous-titre_0__toggle-btn"
type="button"
>
Ceci est un sous titre
</button>
</h4>
<div
class="fr-collapse"
id="ceci-est-un-sous-titre_1234-collapse"
id="ceci-est-un-sous-titre_0-collapse"
>
<div>
<p
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,10 @@ const HiringSimulator = memo(function HiringSimulator({
);
console.log("Event : ", event);
setState({ simulator: "error" });
Sentry.captureMessage(`Erreur durant le chargement de l'iframe brut/net`);
Sentry.captureException(
error ?? new Error("Erreur durant le chargement de l'iframe brut/net"),
{ extra: { source, lineno, colno, event: String(event) } }
);
};

const onLoad = () => {
Expand Down
Loading