Enquête sur un problème de suivi des erreurs
Quand un utilisateur demande « qu'est-ce qui se passe avec cette erreur ? » ou colle une URL d'issue, rassemble le contexte qu'il aurait autrement dû assembler manuellement : qui la rencontre, ce qui a changé, où elle se produit, et si une replay montre la cause.
Outils disponibles
| Outil | Objectif |
|---|---|
posthog:query-error-tracking-issue |
Détails compacts de l'issue (statut, assigné, top frame, release, agrégats) |
posthog:query-error-tracking-issue-events |
Événements $exception échantillonnés avec stack, URL, navigateur, $session_id |
posthog:execute-sql |
Ventilations, corrélations release / flag, événements environnants + logs console autour de l'erreur |
posthog:query-logs |
Entrées de log OTEL autour du timestamp de l'erreur pour les issues serveur |
posthog:query-session-recordings-list |
Replays liées (délègue le classement à finding-replay-for-issue) |
posthog:read-data-schema |
Confirme les clés de propriété avant de les utiliser comme filtre |
Workflow
Étape 1 — Établir la baseline de l'issue
Récupère l'enregistrement de l'issue avec ses agrégats compacts et une sparkline :
posthog:query-error-tracking-issue
{
"issueId": "<issue_id>",
"dateRange": { "date_from": "-30d" },
"includeSparkline": true,
"volumeResolution": 12
}
Capture : name, description, statut, first_seen, last_seen, assignee,
total occurrences / users / sessions, top in-app frame, métadonnées de release
latest, et les buckets de volume.
La sparkline te montre la forme — plate, pic, rampe ou récurrente — et cette forme
guide le reste de l'enquête. Si l'utilisateur a posé uniquement une question de statut,
saute includeSparkline pour économiser des tokens.
Étape 2 — Récupère un événement exception échantillonné
Un événement capturé a les frames de stack, l'URL, le navigateur et les propriétés nécessaires pour raisonner sur la cause. Tire d'abord un échantillon récent, puis un ancien pour comparer.
posthog:query-error-tracking-issue-events
{
"issueId": "<issue_id>",
"limit": 1,
"verbosity": "stack"
}
Utilise verbosity: "raw" seulement si le stack tronqué cache la réponse. L'outil
défaut à onlyAppFrames: true, qui supprime les frames vendor ; bascule à false
quand le bug semble vivre dans une libraire tierce — ou quand la réponse revient avec
stacktrace.type: "resolved" mais pas de frames du tout (courant pour les bundles
minifiés où chaque frame a l'air vendor au resolver, p. ex. React en production).
Pour l'échantillon le plus ancien, resserre dateRange à une fenêtre étroite autour
du first_seen de l'issue (p. ex. définis date_from légèrement avant et date_to
légèrement après) et passe orderDirection: "ASC" pour obtenir l'événement le plus
ancien dans la fenêtre plutôt que le plus récent — l'outil défaut à DESC, ce qui
retournerait un événement récent et dupliquerait silencieusement le premier appel.
Si les événements récents et les plus anciens ont l'air significativement différents —
root de stack différent, pattern d'URL différent — l'issue peut être une erreur de
groupement. Signale-la pour grouping-noisy-errors au lieu de continuer comme s'il
s'agissait d'un bug unique.
Étape 3 — Exécute les ventilations pour isoler la cause
Les ventilations ne sont pas un outil typé — plonge dans execute-sql. Exécute
seulement les ventilations que la forme de l'issue suggère ; chacune coûte une
requête et encombre la synthèse.
| Forme de sparkline | Première ventilation à essayer |
|---|---|
| Pic à partir de zéro | Par app version / release — presque toujours une régression de déploiement (voir ci-dessous) |
| Steady-state élevé | Par navigateur / OS — bug de rendu ou spécifique à la plateforme |
| Rampe | Par géographie ou feature flag — exposition à rollout progressif |
| Rafales puis silence | Par heure du jour ou $current_url — tâche planifiée ou page spécifique |
Choisir la bonne propriété de version
PostHog émet trois champs de forme version. Ils signifient des choses différentes et seul l'un d'entre eux répond à « quelle version de l'app de l'utilisateur a introduit cela ? » :
| Propriété | Ce que c'est | Auto-capturée par | Utilise pour |
|---|---|---|---|
$exception_releases |
Carte de release gérée par Cymbal, indexée par ID de release | Seulement quand le SDK publie les métadonnées de release (p. ex. upload de sourcemap lié à une release) | Attribution de release la plus précise quand présente |
$app_version |
La version de l'app déployée par l'utilisateur | iOS (CFBundleShortVersionString), React Native (Expo / react-native-device-info) |
« Quel déploiement de mon app a introduit cela ? » — la question qui compte pour les utilisateurs |
$lib_version |
La version de la librairie SDK PostHog (p. ex. posthog-js 1.298.0) | Chaque SDK sur chaque événement | La question étroite « l'upgrade du SDK PostHog a-t-il introduit cela ? » |
$lib_version est sur pratiquement chaque événement, ce qui la rend tentante — mais
c'est la version de la librairie PostHog, pas la version de l'app de l'utilisateur.
Un $lib_version constant couplé à un pic signifie que l'utilisateur a expédié une
régression dans son propre code avec le SDK inchangé, ce qui est le cas courant.
Atteins $lib_version seulement quand rien d'autre n'est rempli et que tu demandes
explicitement « l'upgrade de PostHog a-t-il causé cela ? ».
Les projets Web / serveur / Node / Java / Python ne capturent pas automatiquement
$app_version — le client doit la définir (via register, un context provider, ou
before_send). Si la ventilation revient avec une ligne $app_version avec tout NULL,
dis-le explicitement dans la synthèse et suggère au client de le connecter ; revenir à
$exception_releases ou à une timeline par jour par first_seen garde l'enquête en mouvement.
Exemple ($app_version — remplie automatiquement sur mobile, manuellement sur web / serveur) :
posthog:execute-sql
SELECT
properties.$app_version AS app_version,
count() AS occurrences,
uniq(person_id) AS users,
min(timestamp) AS first_seen,
max(timestamp) AS last_seen
FROM events
WHERE event = '$exception'
AND (issue_id = '<issue_id>' OR properties.$exception_issue_id = '<issue_id>')
AND timestamp > now() - INTERVAL 30 DAY
GROUP BY app_version
ORDER BY occurrences DESC
LIMIT 20
Le pattern (issue_id = ... OR properties.$exception_issue_id = ...) reflète la
clause build_issue_where canonique de products/error_tracking/backend/api/query_utils.py.
issue_id est le champ virtuel résolut sur events (il suit les overrides de
fingerprint pour que les issues fusionnées/séparées se routent correctement) ;
properties.$exception_issue_id est la propriété d'événement brute capturée à
l'ingestion. Filtrer seulement sur la propriété compte silencieusement mal les
événements pour les issues qui ont été fusionnées ou séparées.
Si first_seen pour un app_version est beaucoup plus tard que le first_seen
global de l'issue, cette release a introduit ou empiré le bug — signal de root-cause
fort. Si chaque ligne est NULL, le SDK ne rapporte pas une app version sur ce
projet (courant sur web / serveur) — bascule à $exception_releases si le client
expédie des releases, ou revient à une timeline toDate(timestamp).
Quand $exception_releases est rempli, c'est un dict JSON indexé par ID de release.
Il n'y a pas de propriété $release au niveau top ; interroge $exception_releases
directement quand tu as besoin d'attribution de release et que le client l'a câblée.
Répète avec properties.$browser, properties.$os, properties.$current_url, ou
tout feature flag que le projet étiquète les erreurs avec.
Étape 4 — Vérifie l'exposition du feature flag
Si l'utilisateur soupçonne une expérience ou un rollout, vérifiez si les utilisateurs affectés avaient un flag activé quand l'erreur s'est tirée.
Pour énumérer quels flags ont été évalués sur les utilisateurs affectés, analyse la
propriété $active_feature_flags — elle est matérialisée comme une string JSON encodée
dans ClickHouse, donc arrayJoin(properties.$active_feature_flags) directement échouera ;
JSONExtract est le pattern qui marche :
posthog:execute-sql
SELECT
arrayJoin(JSONExtract(toString(properties.$active_feature_flags), 'Array(String)')) AS flag,
count() AS occurrences,
uniq(person_id) AS users
FROM events
WHERE event = '$exception'
AND (issue_id = '<issue_id>' OR properties.$exception_issue_id = '<issue_id>')
AND timestamp > now() - INTERVAL 14 DAY
AND notEmpty(toString(properties.$active_feature_flags))
GROUP BY flag
ORDER BY occurrences DESC
LIMIT 20
Caveat : chaque événement capture chaque clé de flag évalué, donc cette énumération
retourne souvent des counts identiques entre flags et ne te dit pas quel flag
corrèle avec l'erreur — seulement quels étaient activés pour l'utilisateur. Pour
tester réellement une hypothèse, interroge la colonne de valeur par flag
properties.$feature/<flag-key>, qui porte la valeur évaluée (true/false/nom
de variant) :
posthog:execute-sql
SELECT
properties.`$feature/my-flag-key` AS variant,
count() AS occurrences,
uniq(person_id) AS users
FROM events
WHERE event = '$exception'
AND (issue_id = '<issue_id>' OR properties.$exception_issue_id = '<issue_id>')
AND timestamp > now() - INTERVAL 14 DAY
GROUP BY variant
ORDER BY occurrences DESC
Compare la répartition de variant ici à l'exposition globale du projet sur ce même flag dans la même fenêtre. Une représentation disproportionnée d'un variant suggère que le flag est impliqué dans la cause — pas une garantie, mais une hypothèse forte.
Étape 5 — Reconstitue ce qui s'est passé autour de l'erreur
Utilise le $session_id de l'événement échantillonné à l'étape 2 pour tirer
l'activité entourant l'exception. Trois sources s'empilent les unes sur les autres ;
exécute celles qui ont du sens pour le SDK qui a capturé l'erreur.
5a. Événements environnants (SDKs clients par $session_id)
Reflète la timeline de session du frontend ET. Tire les événements personnalisés, les page views et les autres exceptions capturées dans la même session dans une fenêtre ±1h :
posthog:execute-sql
SELECT
uuid,
event,
timestamp,
properties.$lib AS lib,
properties.$current_url AS url
FROM events
WHERE $session_id = '<session_id_from_step_2>'
AND (event = '$exception' OR event = '$pageview' OR left(event, 1) != '$')
AND timestamp >= toDateTime('<error_timestamp>', 'UTC') - INTERVAL 1 HOUR
AND timestamp <= toDateTime('<error_timestamp>', 'UTC') + INTERVAL 1 HOUR
ORDER BY timestamp ASC
LIMIT 100
La clause left(event, 1) != '$' supprime les événements autocapture / système
de PostHog tout en gardant chaque événement personnalisé. Le OR event = '$pageview'/'$exception'
rajoute les deux événements système qui valent la peine d'être vus sur la timeline.
C'est le même filtre que l'UI ET utilise.
Les valeurs $lib mixtes dans l'output sont une fonctionnalité, pas du bruit. Quand
un SDK serveur propage $session_id depuis la requête client (le backend propre de
PostHog fait cela), la timeline affiche l'activité serveur inline avec le côté
navigateur — « les deux SDKs quand disponibles » gratuitement. Scanne la colonne lib
pour voir comment chaque ligne a été produite.
La skill défaut à une fenêtre ±1h parce que c'est ce que l'UI utilise ; élargis-la quand les actions d'une issue sont lentes (longs batch jobs, workers en arrière-plan) ou resserre-la quand seules les secondes juste avant le throw comptent.
5b. Logs console (replay de session web / React Native)
Quand session replay est activée, le pipeline de replay émet les appels console.*
dans la table log_entries étiquetés avec le même session id. Tire-les avec la
fenêtre correspondante :
posthog:execute-sql
SELECT timestamp, level, message
FROM log_entries
WHERE log_source = 'session_replay'
AND log_source_id = '<session_id_from_step_2>'
AND timestamp >= toDateTime('<error_timestamp>', 'UTC') - INTERVAL 1 HOUR
AND timestamp <= toDateTime('<error_timestamp>', 'UTC') + INTERVAL 1 HOUR
ORDER BY timestamp ASC
LIMIT 200
log_source = 'session_replay' est le discriminateur — log_entries est partagée
avec d'autres sources. Les résultats vides sont courants : soit replay n'est pas
activée, soit cette session spécifique n'a pas été enregistrée. Mentionne-le dans
la synthèse plutôt que de le traiter comme un échec.
5c. Logs serveur autour de l'erreur (OTEL via query-logs)
Pour les exceptions côté serveur, corrèle le timestamp d'exception avec les entrées
de log OTEL que le client ingère. Beaucoup de projets ne font pas entrer de logs du
tout — si query-logs retourne rien ou une erreur, dis-le et passe à autre chose.
Découvre les services disponibles en premier avec logs-attribute-values-list quand
tu ne sais pas quel service a produit l'erreur.
posthog:query-logs
{
"query": {
"dateRange": {
"date_from": "<error_timestamp minus 5 minutes>",
"date_to": "<error_timestamp plus 5 minutes>"
},
"severityLevels": ["error", "warn"],
"serviceNames": ["<service.name if known>"],
"limit": 50,
"orderBy": "earliest"
}
}
Caveats qui valent la peine d'être connus avant de compter sur cet output :
- Les logs sont ingérés séparément des événements et ont généralement une rétention plus courte. Les vieilles exceptions peuvent retourner vide même si l'issue est toujours active.
trace_id/span_idreviennent zero-padded ("00000000...") quand non définis. La corrélation basée sur les traces ne marche que pour les requêtes explicitement instrumentées, pas pour chaque événement.service.nameest un attribut de ressource. Resserre avecserviceNamesplutôt qu'unsearchTermlibre quand tu connais le producteur.
5d. Trouve une replay représentative
Délègue à finding-replay-for-issue quand choisir la meilleure session compte —
les issues populaires lient des centaines d'enregistrements, surtout des fragments
courts de crash ou des sessions onglet inactif, et cette skill applique le classement
durée / active-time / recency qui trouve celui le plus probable de montrer la cause.
Délègue aussi quand l'utilisateur demande « une replay » sans spécifier laquelle.
Saute la délégation et tire un enregistrement inline via query-session-recordings-list
avec session_ids à partir des événements exception échantillonnés que tu as déjà
récupérés à l'étape 2 quand seulement une poignée de sessions est liée, l'utilisateur
a déjà nommé une session spécifique, ou n'importe quel exemple qui marche fera l'affaire
(p. ex. prouver que l'erreur se reproduit).
Si aucun path ne retourne un enregistrement, mentionne que session replay peut ne pas être activée pour les utilisateurs affectés — contexte utile, pas un échec.
Étape 6 — Synthèse
Présente dans cet ordre :
- Ce que c'est — type, message, où dans la stack
- Qui ça affecte — total utilisateurs, sessions, et tout breakdown de segment qui s'est démarqué
- Quand ça a commencé —
first_seen, plus la release / version qui l'a introduit si une ventilation en a trouvé une - Cause probable — une ou deux hypothèses appuyées par les ventilations ci-dessus
- Prochaine étape — une action concrète : enquêter sur la release suspecte, regarder la replay liée, ping l'assigné, ou escalader
Garde la synthèse serrée. L'utilisateur veut la réponse, pas une visite des données.
Conseils
- La clé de join canonique des événements vers une issue est le champ virtuel
issue_idrésolu, avecproperties.$exception_issue_idcomme fallback — vois l'étape 3 pour la raison et le patternbuild_issue_where. - Pour une ventilation « quelle version a introduit cela ? », préfère
$app_version(la version de l'app déployée par l'utilisateur, auto-capturée sur iOS / React Native et définie manuellement sur web / serveur) ou$exception_releasesquand rempli. Évite$lib_versionpour cette question — c'est la version de la librairie SDK PostHog, pas l'app de l'utilisateur. Vois la sous-section « Choisir la bonne propriété de version » à l'étape 3. - Si l'issue s'étend sur plus de 30 jours, élargis explicitement la date range. Les
defaults tronquent souvent l'événement
first_seenoriginal de la ventilation. - Ne propose pas de fix dans la synthèse à moins que la cause soit évidente à partir de la sample stack. Les hypothèses appuyées par les données sont plus utiles que les devinettes confiantes.
- Si
query-error-tracking-issueretourne un arrayexternal_issues, l'issue est déjà liée à un ticket Linear / Jira / GitHub. Mentionne le lien dans la synthèse pour que l'utilisateur n'ouvre pas un doublon.