Aller au contenu

Remontée d'exceptions

La remontée d'exceptions est le cœur du SDK. Une fois configuré, chaque exception que Laravel reporte est transmise à votre dashboard.

Capture automatique

Au démarrage, le SDK enregistre un callback reportable() additif sur le handler d'exceptions de l'application :

php
$handler->reportable(function (Throwable $e): void {
    app(Monitor::class)->report($e);
});

Comme le callback est additif, il ne remplace ni ne supprime votre logging existant ni les autres handlers reportable, il se contente d'en transmettre une copie à Quiet Guard. Toute exception qui atteint le handler (exceptions non capturées, ou tout ce que vous passez à report()) est capturée, à l'exception de celle levée en servant une requête qui correspond à ignore_paths : voir Configuration.

La capture est fail-safe : une panne réseau, une mauvaise configuration ou une erreur serveur dans le SDK est avalée et ne remonte jamais vers l'application hôte.

Fail-safe ne veut pas dire muet. Lorsque le serveur refuse un rapport, il dit pourquoi, et cette phrase est écrite dans votre propre log comme avertissement, sous la forme HTTP <statut> from <url>: <message> : un 402 pendant une relance de paiement, un 403 sur une limite de forfait, un 422 sur un champ mal formé. La ligne porte un marqueur interne, elle n'est donc jamais capturée puis transmise à son tour.

Remontée manuelle

Utilisez la façade Monitor pour reporter explicitement une exception gérée :

php
use QuietGuard\LaravelMonitor\Facades\Monitor;

try {
    $this->chargeCustomer($order);
} catch (\Throwable $e) {
    Monitor::report($e);

    // Relancez ou récupérez comme bon vous semble.
    throw $e;
}

Monitor::report() ne lève jamais d'exception. Il ne fait silencieusement rien lorsque la remontée est désactivée, lorsque la clé du projet est absente, lorsque l'environnement courant n'est pas dans la liste blanche, ou lorsque la requête correspond à ignore_paths. À partir de la version 0.3.1, une MONITOR_URL vide ne l'arrête pas : elle vaut le service hébergé, voir Configuration.

Vous pouvez résoudre le même objet depuis le conteneur :

php
app(\QuietGuard\LaravelMonitor\Monitor::class)->report($e);

Ce qui est envoyé

Chaque rapport est posté sur l'endpoint /api/v1/ingest du serveur et contient :

L'exception

  • classe, message, code, fichier et ligne ;
  • la pile d'appels complète par défaut (MONITOR_TRACE_LIMIT=0): chaque frame, pour que les erreurs profondes gardent leur origine ; fixez un nombre de frames pour alléger les payloads. Chaque frame est réduit à file, line, function, class et type (les arguments ne sont jamais envoyés, ils peuvent contenir des secrets) ;
  • à partir de la version 0.3, le site de levée en tête de la pile : le fichier et la ligne de l'exception elle-même voyagent comme frame zéro, puisque la trace de PHP commence à l'appelant et n'a jamais contenu la ligne fautive. MONITOR_TRACE_LIMIT compte cette frame (1 = le site seul) ;
  • un extrait de code (code) sur les frames de votre application : les cinq lignes de part et d'autre de la ligne de la frame, lues dans le fichier qui a tourné, chacune coupée à 500 caractères, sur au plus dix frames par rapport. Les frames d'une dépendance (tout chemin contenant /vendor/) n'en portent jamais. Une ligne qui nomme une clé de la liste scrub ci-dessous (password, secret, token, passphrase...) et lui donne une valeur ($secret = ..., 'api_key' => ..., setApiKey('...')) est remplacée entière par [scrubbed], parce que le code source est l'endroit où vit un secret codé en dur ; les mots key, auth, credential, salt, private, dsn, bearer, sk_ et pk_ comptent aussi, et une valeur qui n'en nomme aucun voyage telle quelle. À partir de la version 0.3.1, un appel statique (Auth::user()) n'est plus pris pour une valeur, si bien que sa ligne reste lisible ; la règle entière est sur la page de masquage du cœur. MONITOR_CODE_SNIPPETS=false n'envoie que le fichier et la ligne.

Le contexte

  • environment, release (depuis MONITOR_RELEASE, ou détectée depuis le checkout quand elle est vide), php_version, laravel_version, occurred_at (ISO 8601) ;
  • source : console en CLI/file, ou http pour les requêtes web.

Pour les requêtes HTTP, le contexte inclut en plus :

  • url : l'adresse complète de la requête. À partir de la version 0.3.1, elle passe au masquage : la valeur de chaque paramètre de sa chaîne de requête nommé dans la liste scrub (le token et la signature d'un lien de réinitialisation signé), et tout segment de chemin fait de quarante lettres ou chiffres d'affilée (le jeton que Laravel place dans /reset-password/{token}), deviennent %5Bscrubbed%5D ; un identifiant numérique, un slug, un UUID ou un ULID restent ;
  • method ;
  • request : en-têtes, query string et corps: le password / password_confirmation du corps étant retirés et l'ensemble passé au masquage. À partir de la version 0.3.1, le masquage entre aussi dans une valeur qui est un objet JSON écrit en chaîne, comme l'instantané (snapshot) qu'un composant Livewire renvoie à chaque requête, et un en-tête écrit avec des tirets est masqué comme son nom à soulignés : php_auth_pw masque php-auth-pw, le mot de passe d'une requête HTTP Basic ;
  • user : uniquement l'identifiant de l'utilisateur authentifié (id), jamais ses attributs.

Limites appliquées avant l'envoi

L'endpoint d'ingestion valide quatre de ces champs et répond 422 pour le rapport entier dès que l'un d'eux est trop long. Le SDK les tronque donc lui-même plutôt que de perdre le rapport, et marque la coupe par … [truncated] :

ChampLimite
exception.message8192 caractères
exception.class255 caractères
exception.file1024 caractères
context.release255 caractères

Ces limites sont atteintes précisément par les rapports que vous voulez le plus lire : une QueryException porte toute la requête SQL et ses bindings dans son message, et une exception levée depuis une classe anonyme a le chemin absolu du fichier dans son nom de classe, ce qui sur un répertoire de release consomme l'essentiel des 255 à lui seul. Tronquer est une réponse avec perte, et c'est la bonne ici : les 8192 premiers caractères d'une requête valent incomparablement mieux qu'un rapport qui disparaît en silence. La coupe compte des caractères et non des octets, si bien qu'un caractère multi-octets n'est jamais coupé en deux, ce qui produirait une charge que le serveur refuserait comme UTF-8 invalide.

Masquage

Avant que toute charge utile ne quitte l'application, les données de requête et de contexte sont masquées récursivement : toute clé dont le nom contient l'un des termes configurés (insensible à la casse) voit sa valeur remplacée par [scrubbed]. À partir de la version 0.3.1, le masquage lit aussi les chaînes qui portent elles-mêmes des valeurs nommées : une chaîne JSON est ouverte et masquée par clé, et une URL perd la valeur de ses paramètres nommés dans la liste ainsi que ses segments de chemin en forme de jeton (voir la page de masquage du cœur).

Les termes par défaut sont :

password, password_confirmation, passphrase, token, signature, secret,
authorization, cookie, php_auth_pw, api_key, access_token,
referer, referrer, x-forwarded-for, x-real-ip, cf-connecting-ip,
true-client-ip, x-client-ip, forwarded

Pour ajouter les vôtres, publiez la config (php artisan vendor:publish --tag=monitor-config) et étendez le tableau scrub dans config/monitor.php :

php
'scrub' => [
    ...config('monitor.scrub'),
    // les vôtres :
    'credit_card',
    'iban',
],

Conservez chaque terme livré, sauf à vouloir l'enlever. Réécrire le tableau à la main, c'est ainsi que passphrase et les en-têtes d'IP visiteur disparaissent de la liste sans bruit.

La correspondance se fait par sous-chaîne : api_key masque donc aussi stripe_api_key, et token masque csrf_token. À partir de la version 0.3.1, un tiret y vaut un souligné : api_key masque aussi l'en-tête x-api-key.

Masquage par forme de la valeur

La liste scrub ci-dessus regarde le nom du champ. Elle ne voit pas une adresse écrite au milieu d'un message d'erreur, dans un segment d'URL, ou dans un champ que quelqu'un a appelé reference.

La liste redact regarde la valeur elle-même, sur votre serveur, avant tout envoi :

php
'redact' => ['email', 'iban', 'nir', 'card', 'phone'],

Les formes qui portent une clé de contrôle sont vérifiées et non simplement reconnues : une référence de commande à seize chiffres n'est pas prise pour un numéro de carte, et un IBAN doit passer le modulo 97. Chaque valeur masquée indique ce qui a été caché, par exemple Utilisateur [redacted:email] introuvable, de sorte que le message reste lisible.

Retirez les motifs qui produisent des faux positifs sur vos données, ou mettez 'redact' => [] pour tout désactiver. Pour vos propres formes :

php
'redact_custom' => ['customer_ref' => '/CUST-\d{6}/'],

Envoi en arrière-plan

Lorsque MONITOR_QUEUE est défini, les exceptions sont dispatchées comme job en file SendExceptionToMonitor (2 tentatives) au lieu d'être envoyées en ligne. Cela sort la remontée du chemin critique de la requête. Voir Configuration.

La charge est masquée avant d'être mise en file, si bien que les formes que le masquage ci-dessus existe pour arrêter ne séjournent jamais en clair dans vos propres tables jobs et failed_jobs. Un job dont l'envoi a échoué lève, en portant ce que le serveur a dit : la seconde tentative a donc réellement lieu, et un échec définitif est enregistré dans failed_jobs au lieu d'être marqué comme réussi.

Regroupement côté serveur

Le SDK ne fait qu'envoyer les données ; le serveur regroupe les occurrences en anomalies par empreinte, rouvre les régressions et corrèle les releases aux commits. Consultez la documentation du serveur pour la manière dont les anomalies sont affichées et gérées.

Vous lisez la documentation Laravel SDK v1.0.