/* * Copyright (c) 2023 Proton AG * * This file is part of ProtonVPN. * * ProtonVPN is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * ProtonVPN is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with ProtonVPN. If not, see . */ using System; using System.Collections.Generic; using System.Diagnostics; using System.Reflection; using System.Runtime.CompilerServices; using ProtonVPN.Common.Legacy.Helpers; using ProtonVPN.IssueReporting.Contracts; using ProtonVPN.Logging.Contracts; using Sentry; namespace ProtonVPN.IssueReporting; public class IssueReporter : IIssueReporter { private readonly HashSet> _exceptionsSent = []; private readonly HashSet> _messagesSent = []; public IssueReporter(ILogger logger) { SentryInitializer.SetLogger(logger); } public void CaptureError( Exception e, [CallerFilePath] string sourceFilePath = "", [CallerMemberName] string sourceMemberName = "", [CallerLineNumber] int sourceLineNumber = 0) { CaptureException(SentryLevel.Error, e, sourceFilePath, sourceMemberName, sourceLineNumber); } private void CaptureException(SentryLevel level, Exception e, string sourceFilePath, string sourceMemberName, int sourceLineNumber) { CallerProfile callerProfile = new(sourceFilePath, sourceMemberName, sourceLineNumber); Tuple tuple = Tuple.Create(callerProfile, e.GetType()?.FullName); if (!_exceptionsSent.Contains(tuple!)) { IEnumerable fingerprint = GenerateExceptionFingerprint(e); SentrySdk.ConfigureScope(scope => { scope.Level = level; scope.SetTag("captured_in", $"{callerProfile.SourceClassName}.{callerProfile.SourceMemberName}:{callerProfile.SourceLineNumber}"); scope.SetFingerprint(fingerprint); SentrySdk.CaptureException(e); }); _exceptionsSent.Add(tuple!); } } private IEnumerable GenerateExceptionFingerprint(Exception e) { string? exceptionTypeName = string.Empty; string? classFullName = string.Empty; string? methodName = string.Empty; int? line = null; try { exceptionTypeName = e.GetType()?.FullName; StackTrace stackTrace = new(e, true); for (int i = 0; i < stackTrace.FrameCount; i++) { StackFrame? frame = stackTrace.GetFrame(i); MethodBase? method = frame?.GetMethod(); classFullName = method?.DeclaringType?.FullName; methodName = method?.Name; line = frame?.GetFileLineNumber(); // Our line of code that ended up triggering the Exception if (classFullName is not null && classFullName.Contains("ProtonVPN")) { break; } } } catch { yield break; } yield return $"Exception: {exceptionTypeName}"; yield return $"Type: {classFullName}"; yield return $"Method: {methodName}()"; yield return $"Line: {line}"; } public void CaptureError(string message, string? description = null) { CaptureMessage(SentryLevel.Error, message, description); } private void CaptureMessage(SentryLevel level, string message, string? description) { Tuple tuple = Tuple.Create(level, message, description); if (!_messagesSent.Contains(tuple!)) { SentrySdk.ConfigureScope(scope => { scope.Level = level; if (!string.IsNullOrWhiteSpace(description)) { scope.TransactionName = description; scope.SetExtra("description", description); } scope.SetFingerprint([message]); SentrySdk.CaptureMessage(message); }); _messagesSent.Add(tuple!); } } public void CaptureMessage(string message, string? description = null) { CaptureMessage(SentryLevel.Info, message, description); } }