investigating-error-issue

Par posthog · skills

Examine de bout en bout un seul problème de suivi d'erreurs PostHog. À utiliser lorsque l'utilisateur fournit un ID de problème ou colle une URL (`/error_tracking/<id>`) et souhaite comprendre l'erreur — qui elle affecte, ce qui la déclenche, quand elle a commencé, si elle est corrélée à une release, un navigateur, un OS ou un feature flag, et quelle devrait être la prochaine étape. Récupère les métriques agrégées, des exemples d'événements d'exception, des ventilations par segment, les replays associés, et synthétise un résumé de niveau hypothèse en une seule passe.

npx skills add https://github.com/posthog/skills --skill investigating-error-issue

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_id reviennent 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.name est un attribut de ressource. Resserre avec serviceNames plutôt qu'un searchTerm libre 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 :

  1. Ce que c'est — type, message, où dans la stack
  2. Qui ça affecte — total utilisateurs, sessions, et tout breakdown de segment qui s'est démarqué
  3. Quand ça a commencéfirst_seen, plus la release / version qui l'a introduit si une ventilation en a trouvé une
  4. Cause probable — une ou deux hypothèses appuyées par les ventilations ci-dessus
  5. 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_id résolu, avec properties.$exception_issue_id comme fallback — vois l'étape 3 pour la raison et le pattern build_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_releases quand rempli. Évite $lib_version pour 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_seen original 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-issue retourne un array external_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.

Skills similaires