Sentry client for .NET 2.0

Logo Sentry

I'm a big fan of the Sentry service which is used to send error reports.

Each time an unexpected error occurs in one of my applications, a certain amount of information is collected (exception type and message, call stack, operating system version, user name...) then sent to Sentry's servers.

Each event is grouped with other similar events (same exception type, same message) to form a "problem". I can then locate the source of the error and correct it without having to wait for the user to give me the details of the problem.

Lately, I had a small update to do on an old application that had been developed at the time with the .NET 2.0 framework. I thought it would be nice to set up error collection with Sentry. So I search NuGet to download and install SharpRaven, which is the C# client for Sentry, into my application. The problem is that it is only available from .NET 4.0. Updating the application proved complicated, so I was about to drop it. But when you think about it, sending the data certainly had to be done with a simple HTTP request and so it couldn't have been that complicated to do. So I started reading the SDK documentation.

A few minutes later I managed to create my first event. It took me a little longer to fine-tune a few details, but in the end I was able to integrate Sentry support into my NET 2.0 application.

Here is the code, maybe it will be useful if you can't, for one reason or another, use 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,
 
            // Converts the property names from "PascalCase" to "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
}

Some remarks on this code.

  • First, I wrote it quickly and I've only been using it for a few days so it may have some imperfections. I wanted it compact and the objective was not to rewrite the official client;
  • I called the class SharpRaven and copied its interface so I could easily substitute the "real" SharpRaven if I finally had to update the application to a newer version of the.NET framework;
  • I use the package Newtonsoft.Json to serialize/deserialize my objects ;
  • I use the new version of client keys (DSN), those where there is no longer the secret;
  • I have copied the for loop used by SharpRaven to browse the different InnerException. I wouldn't have thought of that approach, it's very elegant;
  • To send the POST request, I use the following small utility class :
public static class HttpPost
{
    public static string Send(string uri, string content)
    {
        bool expect100Continue = ServicePointManager.Expect100Continue;
        try
        {
            // To avoid a 417 error with some proxy.
            // http://stackoverflow.com/questions/566437/http-post-returns-the-error-417-expectation-failed-c
            ServicePointManager.Expect100Continue = false;
 
            var rq = (HttpWebRequest) WebRequest.Create(uri);
 
            // To avoid a 407 error.
            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;
        }
    }
}

It's all for today, hoping it can be used by others. Feel free to try Sentry if you haven't already, there is a free plan where the number of daily events is limited.

Etiquettes:

Ajouter un commentaire