Client Sentry pour .NET 2.0

Logo Sentry

Je suis un grand fan du service Sentry qui sert à envoyer des rapports d'erreur.

A chaque fois qu'une erreur inattendue survient dans une de mes applications, un certain nombre d'informations sont collectées (le type et le message de l'exception, la pile d'appel, la version du système d'exploitation, le nom de l'utilisateur...) puis envoyées aux serveurs de Sentry.

Chaque événement est regroupé avec d'autres événements similaires (même type d'exception, même message) pour former un "problème". Je peux ainsi localiser la source de l'erreur et la corriger sans avoir à attendre que l'utilisateur me communique les détails du problème.

Dernièrement, j'avais une petite mise à jour à effectuer sur une ancienne application qui avait été développée à l'époque avec le .NET framework 2.0. Je me suis dit que ce serait bien de mettre en place la collecte des erreurs avec Sentry de façon et je fait donc une recherche dans NuGet pour télécharger et installer SharpRaven, qui est le client C# pour Sentry, dans mon application. Le problème c'est qu'il n'est disponible qu'à partir de .NET 4.0. Mettre à jour l'application s'avérant compliqué, je m'apprêtais à laisser tomber. Mais en y réfléchissant, l'envoi des données devait certainement se faire avec une simple requête HTTP et donc ça ne devait pas être si compliqué que ça à faire. J'ai donc commencé à lire la documentation du SDK.

Quelques minutes plus tard je parvenais à créer mon premier événement. Il m'a fallu un peu plus de temps pour peaufiner quelques détails, mais au final j'étais en mesure d'intégrer le support de Sentry dans mon application .NET 2.0.

Voici le code, peut-être qu'il vous sera utile si vous ne pouvez pas, pour une raison ou une autre, utiliser SharpRaven :

class RavenClient
{
    class Packet
    {
        public string Message { get; set; }
 
        public string Platform { get; set; }
 
        public ExceptionInterface Exception { get;  } = new ExceptionInterface();
 
        public IDictionary<string, string> Modules { get; } = new SortedDictionary<string, string>();
 
        public string Release { get; set; }
 
        public User User { get; } = new User();
 
        public string ServerName { get; set; }
    }
 
    class User
    {
        public string Username { get; set; }
    }
 
    class SentryStackTrace
    {
        public List<Frame> Frames { get; } = new List<Frame>();
    }
 
    class ExceptionValue
    {
        public string Type { get; set; }
 
        public string Value { get; set; }
 
        public SentryStackTrace Stacktrace { get; } = new SentryStackTrace();
    }
 
    class ExceptionInterface
    {
        public List<ExceptionValue> Values { get; } = new List<ExceptionValue>();
    }
 
    class Frame
    {
        public string Filename { get; set; }
 
        public string Function { get; set; }
 
        public string Module { get; set; }
 
        public int Lineno { get; set; }
 
        public int Colno { get; set; }
 
        public string Package { get; set; }
 
        public string Source { get; set; }
 
        public string InstructionOffset { get; set; }
    }
 
    class Response
    {
        public string Id { get; set; }
    }
 
    #region Fields
 
    readonly string uri;
 
    readonly string publicKey;
 
    readonly string projectId;
 
    #endregion
 
    #region Constructors
 
    public RavenClient(string dsn)
    {
        if (dsn == null) throw new ArgumentNullException(nameof(dsn));
 
        var dsnAsUri = new Uri(dsn);
        uri = dsnAsUri.Scheme + "://" + dsnAsUri.Host;
        publicKey = dsnAsUri.UserInfo;
        projectId = dsnAsUri.Segments[dsnAsUri.Segments.Length - 1];
    }
 
    #endregion
 
    #region Methods
 
    public string CaptureException(Exception exception)
    {
        if (exception == null) throw new ArgumentNullException(nameof(exception));
 
        var report = new Packet
        {
            Message = exception.Message,
            Platform = "csharp",
            Release = Assembly.GetExecutingAssembly().GetName().Version?.ToString(),
            User = { Username = Environment.UserName },
            ServerName = Environment.MachineName
        };
 
        foreach (Assembly assembly in AppDomain.CurrentDomain.GetAssemblies())
        {
            string name = assembly.GetName().Name;
            if (name != null) report.Modules[name] = assembly.GetName().Version?.ToString();
        }
 
        for (Exception currentException = exception ; currentException != null; currentException = currentException.InnerException)
        {
            var exceptionValue = new ExceptionValue
            {
                Type = exception.GetType().FullName,
                Value = exception.Message
            };
            report.Exception.Values.Add(exceptionValue);
 
            var st = new StackTrace(exception, fNeedFileInfo: true);
            StackFrame[] stackFrames = st.GetFrames();
            if (stackFrames != null)
            {
                foreach (StackFrame stackFrame in stackFrames)
                {
                    MethodBase method = stackFrame.GetMethod();
                    var frame = new Frame
                    {
                        Filename = stackFrame.GetFileName(),
                        Lineno = stackFrame.GetFileLineNumber(),
                        Colno = stackFrame.GetFileColumnNumber(),
                        Function = method?.Name,
                        Module = method?.DeclaringType?.FullName,
                        Package = method?.DeclaringType?.Assembly.GetName().Name,
                        Source = method?.ToString(),
                        InstructionOffset = "0x" + stackFrame.GetILOffset().ToString("x")
                    };
                    exceptionValue.Stacktrace.Frames.Insert(0, frame);
                }
            }
        }
 
        var contractResolver = new DefaultContractResolver
        {
            NamingStrategy = new SnakeCaseNamingStrategy()
        };
        var jsonSerializer = new JsonSerializer
        {
            NullValueHandling = NullValueHandling.Ignore,
 
            // Permet de convertir les noms des propriétés de "PascalCase" à "snake_case".
            ContractResolver = contractResolver
        };
 
        string packetAsString;
        using (var sw = new StringWriter())
        {
            jsonSerializer.Serialize(sw, report);
            packetAsString = sw.ToString();
        }
        int unixTimestamp = (int) DateTime.UtcNow.Subtract(new DateTime(1970, 1, 1)).TotalSeconds;
        string responseAsString = HttpPost.Send(
            $"{uri}/api/{projectId}/store/?sentry_version=7&sentry_key={publicKey}&sentry_client=wiip/1.0&sentry_timestamp={unixTimestamp}",
            packetAsString);
        var response = JsonConvert.DeserializeObject<Response>(responseAsString);
        return response?.Id;
    }
 
    #endregion
}

Quelques remarques sur ce code.

  • Premièrement, je l'ai écrit en vitesse et je ne l'utilise que depuis quelques jours donc il peut comporter quelques imperfections. Je l'ai voulu compact et j'ai été à l'essentiel, l'objectif n'étant pas de réécrire le client officiel ;
  • J'ai appelé la classe SharpRaven et j'ai repris son interface de façon à pouvoir substituer facilement le "vrai" SharpRaven si je suis amené finalement à mettre à jour l'application vers une version plus récente du .NET framework ;
  • J'utilise le package Newtonsoft.Json pour sérialiser/désérialiser mes objets ;
  • J'utilise la nouvelle version des clés clientes (DSN), celles où il n'y a plus le secret ;
  • J'ai repris la boucle for utilisée par SharpRaven pour parcourir les différentes InnerException. Je n'aurai pas pensé à cette approche, c'est très élégant ;
  • Pour envoyer la requête POST, j'utilise la petite classe utilitaire suivante :
public static class HttpPost
{
    public static string Send(string uri, string content)
    {
        bool expect100Continue = ServicePointManager.Expect100Continue;
        try
        {
            // Pour éviter une erreur 417 avec certains serveurs proxy.
            // http://stackoverflow.com/questions/566437/http-post-returns-the-error-417-expectation-failed-c
            ServicePointManager.Expect100Continue = false;
 
            var rq = (HttpWebRequest) WebRequest.Create(uri);
 
            // Pour éviter erreur 407
            rq.UseDefaultCredentials = true;
 
            rq.ContentType = "application/x-www-form-urlencoded";
            rq.Method = "POST";
            byte[] bytes = Encoding.UTF8.GetBytes(content);
            rq.ContentLength = bytes.Length;
            using (Stream os = rq.GetRequestStream())
            {
                os.Write(bytes, 0, bytes.Length);
            }
 
            using (WebResponse webResponse = rq.GetResponse())
            {
                Stream responseStream = webResponse.GetResponseStream();
                if (responseStream == null) return null;
                using (var sr = new StreamReader(responseStream))
                {
                    return sr.ReadToEnd().Trim();
                }
            }
        }
        finally
        {
            ServicePointManager.Expect100Continue = expect100Continue;
        }
    }
}

Voilà, en espérant que ça puisse servir à d'autres. N'hésitez pas à essayer Sentry si vous ne connaissez pas, il y a un plan gratuit où le nombre d'événements quotidien est limité.

Etiquettes:

Ajouter un commentaire