Journalisation de l’activité utilisateur, télémésortinge (et variables dans les gestionnaires d’exceptions globales)

Contexte:

Je traite avec une très vieille application qui génère des exceptions très rarement et de manière très intermittente.

Pratiques actuelles:

En général, nous, les programmeurs, traitons les rares inconnues à l’aide de gestionnaires d’exception globale, en procédant comme suit:

[STAThread] [SecurityPermission(SecurityAction.Demand, Flags = SecurityPermissionFlag.ControlAppDomain)] private static void Main() { Application.ThreadException += new ThreadExceptionEventHandler(UIThreadException); Application.SetUnhandledExceptionMode(UnhandledExceptionMode.CatchException); AppDomain.CurrentDomain.UnhandledException += new UnhandledExceptionEventHandler(UnhandledException); Application.EnableVisualStyles(); Application.SetCompatibleTextRenderingDefault(false); Application.Run(new OldAppWithLotsOfWierdExceptionsThatUsersAlwaysIgnore()); } private static void UIThreadException(object sender, ThreadExceptionEventArgs t) { //------------------------------- ReportToDevelopers("All the steps & variables you need to repro the problem are: " + ShowMeStepsToReproduceAndDiagnoseProblem(t)); //------------------------------- MessageToUser.Show("It's not you, it's us. This is our fault.\r\n Detailed information about this error has automatically been recorded and we have been notified.Yes, we do look at every error. We even try to fix some of them.") } private static void UnhandledException(object sender, UnhandledExceptionEventArgs e) { //... } 

Domaine du problème:

Il est difficile d’obtenir les étapes de repro des utilisateurs et à cause du nombre de problèmes signalés, je ne veux pas descendre (exception de la deuxième chance) WinDBG ou CDB pour le moment. Je veux des mesures et, espérons-le, un amour récent System.Diagnostic en premier.

Recherche / Compréhension:

Il y a longtemps, j’ai lu un livre intitulé Debugging Microsoft .NET 2.0 Applications , qui décrit un outil génial que John Robbins (alias The BugSlayer) a écrit sur SuperAssert.Net.

Le seul inconvénient de cet outil est (afin de résoudre les problèmes) la taille des images mémoire est énorme et bien sûr, leur débogage est presque autant un art qu’une science.

Question:

J’espère que quelqu’un pourra me dire comment je peux vider les variables de ce programme, du moins celle qui se trouve à la dernière étape des applications Exception.StackTrace .

Est-ce possible de nos jours? Il est assez facile pour moi de mapper le StackTrace sur les actions des utilisateurs pour déterminer les étapes à suivre. J’ai juste besoin des variables!

Mettre à jour

S’est avéré être un routeur défectueux.

Le projet Open Source est maintenant sur GitHub: https://github.com/MeaningOfLights/UserActionLog

J’ai fait une énorme quantité de recherche dans cet *. En fin de compte, je viens de créer un journal de ce que l’utilisateur fait, c’est une fraction de la taille d’un vidage de la mémoire, ce qui m’obtient de façon fiable les étapes à suivre pour reproduire les problèmes. Un autre avantage est également de comprendre comment les utilisateurs utilisent l’application.

* Je ne pouvais vraiment pas trouver quoi que ce soit en ligne qui fasse cette base de journalisation de l’activité utilisateur. Tout ce que j’ai trouvé concernait AOP, Auto UI Testing Frameworks ou des vidages de mémoire de 1/2 gigaoctet.

Pour votre commodité, voici la bonté!


Classe ActionLogger:

 public class ActionLogger { private Type _frmType; private Form _frm; ///  /// Ctor Lazy way of hooking up all form control events to listen for user actions. ///  /// /// The WinForm, WPF, Xamarin, etc Form. public ActionLogger(Control frm) { _frmType = ((Form)frm).GetType(); _frm = (Form)frm; ActionLoggerSetUp(frm); } ///  /// Ctor Optimal way of hooking up control events to listen for user actions. ///  public ActionLogger(Control[] ctrls) { ActionLoggerSetUp(ctrls); } ///  /// Lazy way of hooking up all form control events to listen for user actions. ///  /// /// The WinForm, WPF, Xamarin, etc Form. public void ActionLoggerSetUp(Control frm) { HookUpEvents(frm); //First hook up this controls' events, then traversely Hook Up its children's foreach (Control ctrl in frm.Controls) { ActionLoggerSetUp(ctrl); //Recursively hook up control events via the *Form's* child->child->etc controls } } ///  /// Optimal way of hooking up control events to listen for user actions. ///  /// The controls on the WinForm, WPF, Xamarin, etc Form. public void ActionLoggerSetUp(Control[] ctrls) { foreach (var ctrl in ctrls) { HookUpEvents(ctrl); } } ///  /// Releases the hooked up events (avoiding memory leaks). ///  public void ActionLoggerTierDown(Control frm) { ReleaseEvents(frm); } ///  /// Hooks up the event(s) needed to debug problems. Feel free to add more Controls like ListView for example subscribe LogAction() to more events. ///  /// The control whose events we're suspicious of causing problems. private void HookUpEvents(Control ctrl) { if (ctrl is Form) { Form frm = ((Form)ctrl); frm.Load += LogAction; frm.FormClosed += LogAction; frm.ResizeBegin += LogAction; frm.ResizeEnd += LogAction; } else if (ctrl is TextBoxBase) { TextBoxBase txt = ((TextBoxBase)ctrl); txt.Enter += LogAction; } else if (ctrl is ListControl) { //ListControl stands for ComboBoxes and ListBoxes. ListControl lst = ((ListControl)ctrl); lst.SelectedValueChanged += LogAction; } else if (ctrl is ButtonBase) { //ButtonBase stands for Buttons, CheckBoxes and RadioButtons. ButtonBase btn = ((ButtonBase)ctrl); btn.Click += LogAction; } else if (ctrl is DateTimePicker) { DateTimePicker dtp = ((DateTimePicker)ctrl); dtp.Enter += LogAction; dtp.ValueChanged += LogAction; } else if (ctrl is DataGridView) { DataGridView dgv = ((DataGridView)ctrl); dgv.RowEnter += LogAction; dgv.CellBeginEdit += LogAction; dgv.CellEndEdit += LogAction; } } ///  /// Releases the hooked up events (avoiding memory leaks). ///  ///  private void ReleaseEvents(Control ctrl) { if (ctrl is Form) { Form frm = ((Form)ctrl); frm.Load -= LogAction; frm.FormClosed -= LogAction; frm.ResizeBegin -= LogAction; frm.ResizeEnd -= LogAction; } else if (ctrl is TextBoxBase) { TextBoxBase txt = ((TextBoxBase)ctrl); txt.Enter -= LogAction; } else if (ctrl is ListControl) { ListControl lst = ((ListControl)ctrl); lst.SelectedValueChanged -= LogAction; } else if (ctrl is DateTimePicker) { DateTimePicker dtp = ((DateTimePicker)ctrl); dtp.Enter -= LogAction; dtp.ValueChanged -= LogAction; } else if (ctrl is ButtonBase) { ButtonBase btn = ((ButtonBase)ctrl); btn.Click -= LogAction; } else if (ctrl is DataGridView) { DataGridView dgv = ((DataGridView)ctrl); dgv.RowEnter -= LogAction; dgv.CellBeginEdit -= LogAction; dgv.CellEndEdit -= LogAction; } } ///  /// Log the Control that made the call and its value ///  ///  ///  public void LogAction(object sender, EventArgs e) { if (!(sender is Form || sender is ButtonBase || sender is DataGridView)) //Tailor this line to suit your needs { //dont log control events if its a Maintenance Form and its not in Edit mode if (_frmType.BaseType.ToSsortingng().Contains("frmMaint")) {//This is ssortingctly specific to my project - you will need to rewrite this line and possible the line above too. That's all though... PropertyInfo pi = _frmType.GetProperty("IsEditing"); bool isEditing = (bool)pi.GetValue(_frm, null); if (!isEditing) return; } } StackTrace stackTrace = new StackTrace(); StackFrame[] stackFrames = stackTrace.GetFrames(); var eventType = stackFrames[2].GetMethod().Name;//This depends usually its the 1st Frame but in this particular framework (CSLA) its 2 ActionLog.LogAction(_frm.Name, ((Control)sender).Name, eventType, GetSendingCtrlValue(((Control)sender), eventType)); } private ssortingng GetSendingCtrlValue(Control ctrl, ssortingng eventType) { if (ctrl is TextBoxBase) { return ((TextBoxBase)ctrl).Text; } //else if (ctrl is CheckBox || ctrl is RadioButton) { // return ((ButtonBase)ctrl).Text; //} else if (ctrl is ListControl) { return ((ListControl)ctrl).Text.ToSsortingng(); } else if (ctrl is DateTimePicker) { return ((DateTimePicker)ctrl).Text; } else if (ctrl is DataGridView && eventType == "OnRowEnter") { if (((DataGridView)ctrl).SelectedRows.Count > 0) { return ((DataGridView)ctrl).SelectedRows[0].Cells[0].Value.ToSsortingng(); } else { return ssortingng.Empty; } } else if (ctrl is DataGridView) { DataGridViewCell cell = (((DataGridView)ctrl).CurrentCell); if (cell == null) return ssortingng.Empty; if (cell.Value == null) return ssortingng.Empty; return cell.Value.ToSsortingng(); } return ssortingng.Empty; } } 

Classe ActionLog:

 public static class ActionLog { const ssortingng ACTIONLOGFILEIDENTIFIER = "ActionLog_"; private static int _numberOfDaily = 0; private static int _maxNumerOfLogsInMemory = 512; private static List _TheUserActions = new List(); private static ssortingng _actionLoggerDirectory = ssortingng.Empty; public static void LogActionSetUp(int maxNumerOfLogsInMemory = 512,ssortingng actionLoggerDirectory = "") { if (ssortingng.IsNullOrEmpty(actionLoggerDirectory)) actionLoggerDirectory = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) + "\\Documents\\ProjectNameMgtFolder\\"; if (!Directory.Exists(actionLoggerDirectory)) Directory.CreateDirectory(actionLoggerDirectory); _actionLoggerDirectory = actionLoggerDirectory; LogAction("MDI_Form", "APPLICATION", "STARTUP", ssortingng.Empty); } public static void LogAction(ssortingng frmName, ssortingng ctrlName, ssortingng eventName, ssortingng value) { if (value.Length > 10) value = value.Subssortingng(0, 10); LogAction(DateTime.Now, frmName,ctrlName, eventName, value); } public static void LogAction(DateTime timeStamp, ssortingng frmName, ssortingng ctrlName, ssortingng eventName, ssortingng value) { _TheUserActions.Add(ssortingng.Format("{0}\t{1}\t{2}\t{3}\t{4}", timeStamp.ToShortTimeSsortingng(), frmName, ctrlName, eventName, value)); if (_TheUserActions.Count > _maxNumerOfLogsInMemory) WriteLogActionsToFile(); } public static ssortingng GetLogFileName() { //Check if the current file is > 1 MB and create another ssortingng[] existingFileList = System.IO.Directory.GetFiles(_actionLoggerDirectory, ACTIONLOGFILEIDENTIFIER + DateTime.Now.ToSsortingng("yyyyMMdd") + "*.log"); ssortingng filePath = _actionLoggerDirectory + ACTIONLOGFILEIDENTIFIER + DateTime.Now.ToSsortingng("yyyyMMdd") + "-0.log"; if (existingFileList.Count() > 0) { filePath = _actionLoggerDirectory + ACTIONLOGFILEIDENTIFIER + DateTime.Now.ToSsortingng("yyyyMMdd") + "-" + (existingFileList.Count() - 1).ToSsortingng() + ".log"; FileInfo fi = new FileInfo(filePath); if (fi.Length / 1024 > 1000) //Over a MB (ie > 1000 KBs) { filePath = _actionLoggerDirectory + ACTIONLOGFILEIDENTIFIER + DateTime.Now.ToSsortingng("yyyyMMdd") + "-" + existingFileList.Count().ToSsortingng() + ".log"; } } return filePath; } public static ssortingng[] GetTodaysLogFileNames() { ssortingng[] existingFileList = System.IO.Directory.GetFiles(_actionLoggerDirectory, ACTIONLOGFILEIDENTIFIER + DateTime.Now.ToSsortingng("yyyyMMdd") + "*.log"); return existingFileList; } public static void WriteLogActionsToFile() { ssortingng logFilePath = GetLogFileName(); if (File.Exists(logFilePath)) { File.AppendAllLines(logFilePath,_TheUserActions); } else { File.WriteAllLines(logFilePath,_TheUserActions); } _TheUserActions = new List(); } } 

Remarque: la méthode LogAction se déclenchera probablement en deuxième (par exemple, pour un clic sur un bouton, elle sera invoquée après l’appel de l’événement Button_Click). Ainsi, alors que vous pensez peut-être que vous devez insérer ces événements LogAction à déclencher en premier, par exemple, en inversant l’ordre d’invocation des événements, ce n’est pas une bonne pratique ni nécessaire. L’astuce est dans le stacktrace, le dernier appel de la stack vous indiquera la dernière action de l’utilisateur . Le journal des actions vous indique comment obtenir le programme dans l’état avant que l’exception non gérée ne se produise. Une fois que vous y êtes parvenu, vous devez suivre StackTrace pour mettre en défaut l’application.

Mise en action – par exemple, un événement de chargement de formulaire MDI:

 UserActionLog.ActionLog.LogActionSetUp(); 

Dans l’événement MDI Forms Close:

 UserActionLog.ActionLog.WriteLogActionsToFile(); 

Dans un constructeur de formulaire enfant:

 _logger = New UserActionLog.ActionLogger(this); 

Dans un événement fermé de formulaire enfant:

 _logger.ActionLoggerTierDown(this); 

Dans les événements UIThreadException et CurrentDomain_UnhandledException , appelez WriteLogActionsToFile(); puis attachez les journaux au courrier électronique envoyé au support avec une capture d’écran …


Voici un exemple rapide sur la façon d’envoyer par courrier électronique des fichiers journaux au support:

 ssortingng _errMsg = new System.Text.SsortingngBuilder(); ssortingng _caseNumber = IO.Path.GetRandomFileName.Subssortingng(0, 5).ToUpper(); ssortingng _errorType; ssortingng _screenshotPath; List _emailAttachments = new List(); ssortingng _userName; private static void UIThreadException(object sender, ThreadExceptionEventArgs t) { _errorType = "UI Thread Exception" .... //HTML table containing the Exception details for the body of the support email _errMsg.Append(""); _errMsg.Append(""); _errMsg.Append(""); if (exception != null) { _errMsg.Append(""); if (exception.InnerException != null) _errMsg.Append(""); _errMsg.Append("
User:" & _userName & "
Time:" & _errorDateTime.ToShortTimeSsortingng & "
Exception Type:" & _errorType.ToSsortingng & "
Message:" & exception.Message.Replace(" at ", " at
") & "
Inner Exception:" & exception.InnerException.Message & "
Stacktrace:" & exception.StackTrace & "
"); } .... //Write out the logs in memory to file UserActionLog.ActionLog.WriteLogActionsToFile(); //Get list of today's log files _emailAttachments.AddRange(UserActionLog.ActionLog.GetTodaysLogFileNames()); //Adding a screenshot of the broken window for support is a good touch //https://stackoverflow.com/a/1163770/495455 _emailAttachments.Add(_screenshotPath); .... Email emailSystem = New Email(); //(using Microsoft.Exchange.WebServices.Data) emailSystem.SendEmail(ConfigMgr.AppSettings.GetSetting("EmailSupport"), "PROJECT_NAME - PROBLEM CASE ID: " & _caseNumber, _errMsg.ToSsortingng(), _emailAttachments.ToArray());

Une fois le courrier électronique envoyé, affichez une fenêtre expliquant aux utilisateurs qu’un problème est survenu, avec une belle image … Les sites Web de StackExchange ont un bon exemple, c’est mon préféré: https://serverfault.com/error