Sentry-javascript: Erreurs de chargement et de capture asynchrones

Créé le 18 déc. 2013  ·  36Commentaires  ·  Source: getsentry/sentry-javascript

Je veux pouvoir charger raven.js de manière asynchrone, mais toujours pouvoir capturer les erreurs pendant le chargement du script. (Quelque chose comme la façon dont Google Analytics gère les événements en les stockant dans une variable jusqu'au chargement de la bibliothèque).

Voici ce que j'ai jusqu'à présent : https://gist.github.com/karolisdzeja/8010574

Mais en procédant ainsi, vous perdez une partie des informations et des détails que Raven fournit habituellement. Existe-t-il un moyen de stocker toutes les informations d'erreur de la même manière ?

Feature

Commentaire le plus utile

Voici l'extrait que nous utilisons pour mettre en file d'attente les appels aux méthodes Raven de manière transparente : https://gist.github.com/Kl0tl/ed0a9e74462a2294f4c8842f5389d8ea.

La maquette pourrait certainement être améliorée mais nous n'avons pas besoin de reproduire plus de fonctionnalités. Object.defineProperty nous permet de nous accrocher juste après que Raven se

Tous les 36 commentaires

+1

+1

+1

+1

Tous ceux qui ont commenté cela – qu'est-ce qui ne va pas avec la solution de @karolisdzeja ?

En fin de compte, je ne sais pas comment nous pouvons ajouter une fonctionnalité à la source Raven.js qui est censée fonctionner lorsque la source Raven.js n'est pas sur la page. Je pense que ce sera toujours en fin de compte une solution personnalisée ; au mieux, nous pourrions ajouter un "comment faire" à nos documents.

@benvinegar la solution a l'air bien, mais ce serait mieux si cela était officiellement pris en charge et documenté. Je suis plus heureux de devoir faire confiance à l'équipe Sentry pour évaluer un élément aléatoire.

En fait, une bien meilleure solution serait quelque chose comme le code JS SDK de Twitter : https://dev.twitter.com/web/javascript/loading

Configurez une file d'attente de fonctions au chargement de la page qui est ensuite consommée lorsque le js externe a été chargé, remplaçant l'objet proxy. Et assurez-vous que tous les appels d'API passent par quelque chose comme un appel .ready() au proxy.

Cela garantit que tout appel peut être mis en file d'attente avant que le js ne soit chargé plutôt que simplement capturerMessage, sans avoir à proxy chaque fonction individuellement.

J'aimerais pouvoir charger simplement raven.js de manière asynchrone/différée et ne pas avoir à m'inquiéter.

Autres problèmes avec l'essentiel : il écrase window.onerror et introduit quelques variables globales non contenues.

Il n'utilise pas non plus la fonction complète traceKitWindowOnError que raven.js utilise lors de son chargement.

J'ai légèrement refait l'essentiel ci-dessus : https://gist.github.com/oroce/ec3786ba7eff59963842220c3ffc56b4

Il n'y a pas de variable de fuite. J'ai gardé le gestionnaire window.onerror , mais n'hésitez pas à utiliser window.addEventListener('error', fn) .

La plus grande aide à ce stade est d'avoir traceKitWindowOnError tant que fonction exportée de Raven. Puisque c'est la fonction qui est appelée lorsqu'une erreur se produit : https://github.com/getsentry/raven-js/blob/master/dist/raven.js#L2074

Je sais que nous n'aurions pas cette trace de pile très appropriée, mais nous aurions quelque chose de mieux que ce que nous avons actuellement.

Il y a d'autres inconvénients à faire cela :

  • En s'appuyant sur window.onerror pour détecter les erreurs avant le chargement de Raven, les stacktraces ne sont pas disponibles pour tous les navigateurs

    • en plus de ne pas avoir de trace de pile, cela affectera négativement le regroupement

    • c'est pourquoi install() essaie/attrape l'instrumentation

  • Les traces synthétiques ne fonctionneront pas (elles apparaîtront toutes comme provenant de ce code)
  • Pas de fil d'Ariane

Ainsi, vous échangez des performances potentiellement meilleures contre des rapports d'erreurs de moindre qualité. Si l'exécution asynchrone est importante, je vous recommanderais plutôt de regrouper Raven avec votre propre code afin qu'il soit servi ensemble.

@benvinegar tu as tout à fait raison. Dans les applications qui ne sont pas publiques (c'est-à-dire que google n'atteindra pas les pages), la méthode corbeau classique (bloquante) convient parfaitement, mais dès que vous avez un site public où les points d'information de la page google sont importants, nous devons optimiser comment nous chargeons le code tiers (c'est le prix que nous sommes prêts à payer en faveur de l'ux, de la vitesse et d'une meilleure position dans les résultats de recherche).

De plus, regrouper corbeau dans notre bundle est une solution, mais dès que vous commencez à optimiser votre code frontend avec des optimisations au-dessus du pli, comme diviser votre bundle en plusieurs à l'aide d'outils comme factor-bundle ou vous incluez plusieurs bundles pour gagner plus de vitesse, la solution ci-dessus peut être une meilleure imo, mais je suis ouvert aux suggestions.

Ils ont tous des compromis, donc ce serait formidable si nous pouvions documenter toutes les stratégies disponibles, donc en fonction de chaque application Web spécifique, nous appliquerons différentes stratégies.
Avec la stratégie asynchrone, nous devons charger le script après l'événement onload , au lieu de charger uniquement l'async, pour éviter de bloquer l'événement onload , le rendu.

/**
 * Setup Js error lazy tracking
 * - Pros: doesn't block rendering, onload event
 * - Cons: lower quality error reports for lazy errors
 *
 * <strong i="9">@author</strong> vinhlh
 *
 * <strong i="10">@param</strong>  {object} window
 * <strong i="11">@param</strong>  {object} labJs
 * <strong i="12">@param</strong>  {string} ravenCdn
 * <strong i="13">@param</strong>  {string} sentryDsn
 */
(function(window, labJs, ravenCdn, sentryDsn) {
  var errors = [];
  var oldOnError = window.onerror;

  window.onerror = function() {
    errors.push(arguments);
    oldOnError && oldOnError.apply(this, arguments);
  };
  window.addEventListener('load', function() {
    labJs
      .script(ravenCdn)
      .wait(function() {
        window.onerror = oldOnError;
        Raven.config(sentryDsn).install();
        errors.forEach(function(args) {
          window.onerror.apply(this, args);
        });
      });
  });
})(window, $LAB, 'raven-js-3.8.1/dist/raven.js', 'https://[email protected]/9');

Je pense que nous allons probablement simplement documenter un extrait asynchrone, comme ceux fournis ci-dessus, mais mentionner qu'il comporte des compromis.

Juste un autre commentaire. Ces compromis peuvent sembler acceptables, mais je traite de nombreux tickets d'assistance d'utilisateurs concernant des erreurs de faible fidélité qu'ils rencontrent et qui sont (à tort) supposées dériver de Raven.js. Ma crainte est que si j'encourage les gens à utiliser l'approche asynchrone, j'aurai de plus en plus de gens qui me demanderont "pourquoi n'y a-t-il pas de trace de pile" et d'autres plaintes quand c'est parce que cette approche est de moindre fidélité. Je suis prêt à l'accepter, mais c'est une pilule difficile à avaler. ??

@benvinegar Je

@oroce - oui, ce n'est pas à 100% un problème pour les personnes de ce fil, mais pour les personnes qui pourraient poursuivre cette stratégie sans comprendre correctement les mises en garde (par exemple, il suffit de copier/coller).

Je garderai ce problème ouvert, avec un plan pour ajouter l'extrait aux documents Install - et je mettrai un tas d'avertissements partout.

Merci encore pour votre participation ici / m'avoir convaincu de le faire.

Voici l'extrait que nous utilisons pour mettre en file d'attente les appels aux méthodes Raven de manière transparente : https://gist.github.com/Kl0tl/ed0a9e74462a2294f4c8842f5389d8ea.

La maquette pourrait certainement être améliorée mais nous n'avons pas besoin de reproduire plus de fonctionnalités. Object.defineProperty nous permet de nous accrocher juste après que Raven se

Hé les gars, je me demande juste s'il y a quelque chose qui ne va pas avec la façon dont Raygun le fait de manière asynchrone?
Je ne suis pas sûr, mais cela semble bien gérer les cas extrêmes ? Je me trompe peut-être quand même :)

@Kl0tl très sympa, merci

C'est très simple en utilisant une importation dynamique . Toujours en stage3 mais supporté par webpack.

Nous utilisons simplement la promesse comme une file d'attente. Une fois rempli, tous les rappels sont exécutés dans l'ordre.

const RavenPromise = import('raven-js'); // async load raven bundle

// initial setup
RavenPromise.then(Raven => {
    Raven.config('url-to-sentry', options).install();
}):

// exported log function
export const logMessage = (level, logger, text) => {
    RavenPromise.then(Raven => {
        Raven.captureMessage(text, {level, logger});
    });
};

De la même manière que Google Analytics . Voici un exemple pour analytics.js :

<script async src="https://www.google-analytics.com/analytics.js"></script>
<script>
    window.ga = window.ga || function () {
        (ga.q = ga.q || []).push(arguments)
    }
    ga.l = +new Date

    ga('create', 'UA-XXXXX-Y', 'auto')
    ga('send', 'pageview')
</script>

@benvinegar Est-ce que quelque chose comme ça est possible avec Raven.js ? Peut-être à l'avenir?

@kireerik, il sera certainement mis en œuvre (très probablement comme une documentation pratique), mais je ne peux pas vous donner une estimation précise de la date maintenant.

@kamilogorek Sonne bien (je n'aime pas la solution de contournement). Aucun problème!

Pour toute personne intéressée, j'ai mis en place un autre moyen de charger ravenjs de manière asynchrone :
https://gist.github.com/MaxMilton/e2338b02b7381fc7bef2ccd96f434201

Il est basé sur le code de @oroce, mais les principales différences sont que j'utilise une balise <script async src'='..."> régulière dans l'en-tête du document pour de meilleures performances (les navigateurs peuvent planifier la récupération de la ressource plus tôt) + je ne prends pas la peine de l'envelopper un IIFE et d'autres petits ajustements.

@MaxMilton Joli coup ! J'ai créé ma propre saveur basée sur la vôtre:

<script async src="https://cdn.ravenjs.com/3.22.1/raven.min.js" crossorigin="anonymous" id="raven"></script>
<script>
    (function (sentryDataSourceName) {
        var raven = document.getElementById('raven')
        , isNotLoaded = true
        , errors = []
        raven.onreadystatechange = raven.onload = function () {
            if (isNotLoaded) {
                Raven.config(sentryDataSourceName).install()
                isNotLoaded = !isNotLoaded
                errors.forEach(function (error) {
                    Raven.captureException(error[4] || new Error(error[0]), {
                        extra: {
                            file: error[1]
                            , line: error[2]
                            , col: error[3]
                        }
                    })
                })
            }
        }
        window.onerror = function (message, source, lineNumber, colmnNumber, error) {
            if (isNotLoaded)
                errors.push([message, source, lineNumber, colmnNumber, error])
        }
    })('https://<key>@sentry.io/<project>')
</script>

J'ai aussi une question :

  • Est-il nécessaire de définir l'attribut crossorigin sur la balise script ?
  • Suffit-il de passer jsut l'objet error au lieu de l' autre solution ?

Qu'en penses-tu? Quelle est l'opinion de l'auteur (@kamilogorek) à ce sujet ?

@kireerik lorsque vous mettez crossorigin="anonymous" sur des scripts, cela vous permet de capturer entièrement les erreurs (de ce script externe) avec l'événement window.onerror . Cela empêche également le navigateur d'envoyer des informations d'identification avec la demande d'extraction, ce qui est généralement ce que vous voulez avec des ressources tierces. Référence MDN 1 , référence MDN 2 .

Vous pouvez simplement transmettre l'erreur et cela fonctionnera la plupart du temps. La mise en garde étant que les anciens navigateurs (par exemple Firefox avant la version 31) ne transmettent pas les propriétés columnNo ou Error Object à l'événement window.onerror . Donc, si vous voulez une très bonne compatibilité, vous devez faire ce petit plus. Référence MDN .

EDIT : Astuce bonus : il s'avère que lorsque vous mettez crossorigin sans aucune valeur, il est traité de la même manière que crossorigin="anonymous" .

Pour info, j'ai mis à jour mon idée précédente pour quelque chose qui est beaucoup plus prêt pour la production :

  • beaucoup de commentaires pour expliquer ce qui se passe réellement
  • gros nettoyage + utiliser des noms de variables descriptives (toujours un bon bonus :wink: )
  • envelopper dans IIFE pour ne pas polluer l'espace de noms global
  • corriger les paramètres incorrects transmis à l'élément de tableau d'erreur

Voir l'essentiel si vous voulez tout comprendre OU si vous préférez un copier+coller rapide, voici la version minifiée :

<!-- Sentry JS error tracking -->
<script async src="https://cdn.ravenjs.com/3.22.0/raven.min.js" crossorigin id="raven"></script>
<script>
(function(b,e,c,d){b.onerror=function(a,b,d,f,g){c||e.push([a,b,d,f,g])};b.onunhandledrejection=function(a){c||e.push([a.reason.reason||a.reason.message,a.type,JSON.stringify(a.reason)])};d.onreadystatechange=d.onload=function(){c||(c=!0,
Raven.config("___PUBLIC_DSN___").install(),
b.onunhandledrejection=function(a){Raven.captureException(Error(a.reason.reason||a.reason.message),{extra:{type:a.type,reason:JSON.stringify(a.reason)}})},e.forEach(function(a){Raven.captureException(a[4]||Error(a[0]),{extra:{file:a[1],line:a[2],col:a[3]}})}))}})(window,[],!1,document.getElementById("raven"));
</script>

<link rel="preconnect" href="https://sentry.io">

Remplacez ___PUBLIC_DSN___ par votre DSN et collez-le quelque part dans la tête près de votre balise de fermeture </head> . Ou si vous êtes un hipster qui n'utilise plus les balises <head> et <body> collez-les simplement en haut après toutes les ressources critiques/d'application (par exemple CSS). Idéalement, il devrait être avant tout autre code JavaScript afin que vous puissiez capturer les erreurs des scripts chargés après ce code.

Dans mes tests rapides, il n'y a eu aucun problème, donc je ne vois aucune raison de ne pas l'utiliser par rapport à la version synchrone par défaut.

Si quelqu'un a une idée pour une meilleure approche, je suis impatient de l'entendre.

Edit : Désolé d'avoir édité ce commentaire si souvent. C'est à un niveau stable maintenant. C'était amusant de l'amener dans cet état ! :souriant:

Une fois la bibliothèque sentinelle chargée, la qualité du rapport d'erreur est exactement la même que lors du chargement de la synchronisation ? (Je suppose que oui, je vérifie juste)

De plus, dans les documents que vous voudrez peut-être ajouter, vous ne pouvez pas utiliser Raven tant que la bibliothèque n'est pas chargée, peut-être fournir une fonction de rappel dans les options afin que vous puissiez définir le contexte utilisateur, etc.

d'accord avec @dalyr95 . une fonction de rappel serait utile. peut-être un événement de charge de corbeau personnalisé.

J'ai une demande similaire à @dalyr95. À l'heure actuelle, la seule façon d'appeler setUserContext() est de modifier l'extrait de code du chargeur qui n'est pas aussi propre que de pouvoir passer un rappel sur l'objet de configuration principal.

Bonjour, merci d'avoir signalé le problème.

Nous sommes en train de travailler sur la prochaine version majeure de notre SDK.
Pour cette raison, nous avons dû suspendre le travail sur la version actuelle (sauf bugs majeurs ou de sécurité).
Nous essaierons de revenir à tous les rapports dès que possible, alors soyez patient.

Merci pour votre compréhension,
Acclamations!

Ma solution consistait à ajouter 'undefined'!=k.setup&&k.setup(); immédiatement après l'appel à .install() , puis j'ai ajouté une fonction appelée setup à SENTRY_SDK avec mon code de post-init.

Avec le chargeur asynchrone, j'ai pu définir le contexte utilisateur et d'autres informations en le passant comme deuxième argument à Raven.config :

<script>
    Raven.config("https://<mydsn>@sentry.io/<projectid>", 
      {"release":"0.3.1",
       "environment":"dev",
       "user": {"id":"7b031fa0-32ff-46fe-b94b-e6bc201c3c5f",
                "organisation-id":"b1a50c41-b85e-4c50-9cec-c55ff36cf6d1"}}).install();
</script>

Je pense que tout existe déjà pour cela, cela pourrait juste être mieux documenté.

@danielcompton ne peut obtenir des informations sur l'utilisateur que via l'API principale ?

Cette page vous a été utile?
0 / 5 - 0 notes