c# - Registro de actividad del usuario, telemetría(y variables en controladores de excepciones globales)
.net vb.net (1)
Fondo:
Estoy tratando con una aplicación muy antigua que genera Excepciones muy raramente y de manera intermitente.
Prácticas actuales:
Por lo general, los programadores manejamos incógnitas raras utilizando controladores de excepción global, conectando algo como esto:
[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)
{
//...
}
Dominio del problema:
Es difícil obtener los pasos de reprobación de los usuarios y, debido a la cantidad variada de problemas que se informan, no quiero seguir la ruta de solución de problemas WinDBG o CDB (la excepción de la segunda oportunidad) por el momento. Quiero algunas métricas y, con suerte, un poco de System.Diagnostic amor primero.
Investigación / comprensión:
Hace mucho tiempo leí un libro Depuración de aplicaciones de Microsoft .NET 2.0 y analiza una herramienta genial que John Robbins (también conocido como The BugSlayer) escribió SuperAssert.Net
El único inconveniente de esta herramienta es (para resolver problemas) el tamaño de los volcados de memoria son enormes y, por supuesto, depurarlos es casi un arte, ya que es una ciencia.
Pregunta:
Espero que alguien pueda decirme una forma en que pueda deshacer las variables en este programa, bueno, al menos, en el último paso de las aplicaciones
Exception.StackTrace
.
¿Es esto posible en estos días? Es bastante fácil para mí asignar el StackTrace a las acciones del usuario para resolver los pasos ... ¡Solo necesito las variables!
Actualizar
Resultó ser un enrutador defectuoso.
El proyecto de código abierto ahora está en GitHub: https://github.com/MeaningOfLights/UserActionLog
...
Hice una gran cantidad de investigación sobre esto *. Al final, acabo de crear un registro de lo que hace el usuario, es una fracción del tamaño de un volcado de memoria y de manera confiable me da los pasos para reproducir problemas. También sirve otro beneficio, comprender cómo los usuarios usan la aplicación.
* En serio, no pude encontrar nada en línea que haga este registro básico de actividad del usuario. Todo lo que encontré fue sobre AOP, Auto UI Testing Frameworks o volcados de memoria de 1/2 Gig.
Para su conveniencia aquí está la bondad!
Clase ActionLogger:
public class ActionLogger
{
private Type _frmType;
private Form _frm;
/// <summary>
/// Ctor Lazy way of hooking up all form control events to listen for user actions.
/// </summary>
/// /// <param name="frm">The WinForm, WPF, Xamarin, etc Form.</param>
public ActionLogger(Control frm)
{
_frmType = ((Form)frm).GetType();
_frm = (Form)frm;
ActionLoggerSetUp(frm);
}
/// <summary>
/// Ctor Optimal way of hooking up control events to listen for user actions.
/// </summary>
public ActionLogger(Control[] ctrls)
{
ActionLoggerSetUp(ctrls);
}
/// <summary>
/// Lazy way of hooking up all form control events to listen for user actions.
/// </summary>
/// /// <param name="frm">The WinForm, WPF, Xamarin, etc Form.</param>
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
}
}
/// <summary>
/// Optimal way of hooking up control events to listen for user actions.
/// </summary>
/// <param name="ctrls">The controls on the WinForm, WPF, Xamarin, etc Form.<param>
public void ActionLoggerSetUp(Control[] ctrls)
{
foreach (var ctrl in ctrls) {
HookUpEvents(ctrl);
}
}
/// <summary>
/// Releases the hooked up events (avoiding memory leaks).
/// </summary>
public void ActionLoggerTierDown(Control frm)
{
ReleaseEvents(frm);
}
/// <summary>
/// Hooks up the event(s) needed to debug problems. Feel free to add more Controls like ListView for example subscribe LogAction() to more events.
/// </summary>
/// <param name="ctrl">The control whose events we''re suspicious of causing problems.</param>
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;
}
}
/// <summary>
/// Releases the hooked up events (avoiding memory leaks).
/// </summary>
/// <param name="ctrl"></param>
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;
}
}
/// <summary>
/// Log the Control that made the call and its value
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
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.ToString().Contains("frmMaint")) {//This is strictly 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 string GetSendingCtrlValue(Control ctrl, string 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.ToString();
}
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.ToString();
}
else {
return string.Empty;
}
}
else if (ctrl is DataGridView) {
DataGridViewCell cell = (((DataGridView)ctrl).CurrentCell);
if (cell == null) return string.Empty;
if (cell.Value == null) return string.Empty;
return cell.Value.ToString();
}
return string.Empty;
}
}
Clase ActionLog:
public static class ActionLog
{
const string ACTIONLOGFILEIDENTIFIER = "ActionLog_";
private static int _numberOfDaily = 0;
private static int _maxNumerOfLogsInMemory = 512;
private static List<string> _TheUserActions = new List<string>();
private static string _actionLoggerDirectory = string.Empty;
public static void LogActionSetUp(int maxNumerOfLogsInMemory = 512,string actionLoggerDirectory = "")
{
if (string.IsNullOrEmpty(actionLoggerDirectory)) actionLoggerDirectory = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) + "//Documents//ProjectNameMgtFolder//";
if (!Directory.Exists(actionLoggerDirectory)) Directory.CreateDirectory(actionLoggerDirectory);
_actionLoggerDirectory = actionLoggerDirectory;
LogAction("MDI_Form", "APPLICATION", "STARTUP", string.Empty);
}
public static void LogAction(string frmName, string ctrlName, string eventName, string value)
{
if (value.Length > 10) value = value.Substring(0, 10);
LogAction(DateTime.Now, frmName,ctrlName, eventName, value);
}
public static void LogAction(DateTime timeStamp, string frmName, string ctrlName, string eventName, string value)
{
_TheUserActions.Add(string.Format("{0}/t{1}/t{2}/t{3}/t{4}", timeStamp.ToShortTimeString(), frmName, ctrlName, eventName, value));
if (_TheUserActions.Count > _maxNumerOfLogsInMemory) WriteLogActionsToFile();
}
public static string GetLogFileName()
{
//Check if the current file is > 1 MB and create another
string[] existingFileList = System.IO.Directory.GetFiles(_actionLoggerDirectory, ACTIONLOGFILEIDENTIFIER + DateTime.Now.ToString("yyyyMMdd") + "*.log");
string filePath = _actionLoggerDirectory + ACTIONLOGFILEIDENTIFIER + DateTime.Now.ToString("yyyyMMdd") + "-0.log";
if (existingFileList.Count() > 0)
{
filePath = _actionLoggerDirectory + ACTIONLOGFILEIDENTIFIER + DateTime.Now.ToString("yyyyMMdd") + "-" + (existingFileList.Count() - 1).ToString() + ".log";
FileInfo fi = new FileInfo(filePath);
if (fi.Length / 1024 > 1000) //Over a MB (ie > 1000 KBs)
{
filePath = _actionLoggerDirectory + ACTIONLOGFILEIDENTIFIER + DateTime.Now.ToString("yyyyMMdd") + "-" + existingFileList.Count().ToString() + ".log";
}
}
return filePath;
}
public static string[] GetTodaysLogFileNames()
{
string[] existingFileList = System.IO.Directory.GetFiles(_actionLoggerDirectory, ACTIONLOGFILEIDENTIFIER + DateTime.Now.ToString("yyyyMMdd") + "*.log");
return existingFileList;
}
public static void WriteLogActionsToFile()
{
string logFilePath = GetLogFileName();
if (File.Exists(logFilePath)) {
File.AppendAllLines(logFilePath,_TheUserActions);
}
else {
File.WriteAllLines(logFilePath,_TheUserActions);
}
_TheUserActions = new List<string>();
}
}
Nota: Lo más probable es que el método LogAction se active en segundo lugar (por ejemplo, para hacer clic en un botón, se invocará después de que se haya llamado al evento Button_Click). Entonces, si bien puede pensar que necesita insertar estos eventos de LogAction para disparar primero, por ejemplo, invirtiendo el orden de invocación de eventos que no es una buena práctica y no es obligatorio. El truco está en la pila de seguimiento, las últimas llamadas en la pila le indicarán la última acción del usuario . El Registro de acciones le indica cómo llevar el programa al estado antes de que ocurriera la excepción no controlada. Una vez que llegue a ese punto, debe seguir el StackTrace para que falle la aplicación.
Ponerlo en acción, por ejemplo, un evento MDI Form Load:
UserActionLog.ActionLog.LogActionSetUp();
En el evento MDI Forms Close:
UserActionLog.ActionLog.WriteLogActionsToFile();
En un constructor de formularios hijo:
_logger = New UserActionLog.ActionLogger(this);
En un evento cerrado de formulario secundario:
_logger.ActionLoggerTierDown(this);
En los eventos
UIThreadException
y
CurrentDomain_UnhandledException
, llame a
WriteLogActionsToFile();
luego adjunte los registros al correo electrónico enviado al soporte con una captura de pantalla ...
Aquí hay un ejemplo rápido sobre cómo enviar por correo electrónico archivos de registro para que sean compatibles:
string _errMsg = new System.Text.StringBuilder();
string _caseNumber = IO.Path.GetRandomFileName.Substring(0, 5).ToUpper();
string _errorType;
string _screenshotPath;
List<string> _emailAttachments = new List<string>();
string _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("<table><tr><td colSpan=1><b>User:</b></td><td colSpan=2>" & _userName & "</td></tr>");
_errMsg.Append("<tr><td><b>Time:</b></td><td>" & _errorDateTime.ToShortTimeString & "</td></tr><tr></tr>");
_errMsg.Append("<tr><td><b>Exception Type:</b></td><td>" & _errorType.ToString & "</td></tr><tr></tr>");
if (exception != null) {
_errMsg.Append("<tr><td><b>Message:</b></td><td>" & exception.Message.Replace(" at ", " at <br>") & "</td></tr><tr></tr>");
if (exception.InnerException != null) _errMsg.Append("<tr><td><b>Inner Exception:</b></td><td>" & exception.InnerException.Message & "</td></tr>");
_errMsg.Append("<tr><td><b>Stacktrace:</b></td><td>" & exception.StackTrace & "</td></tr></table>");
}
....
//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://.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.ToString(), _emailAttachments.ToArray());
Después de enviar el correo electrónico, muestre a los usuarios una ventana que explica que ocurrió un problema, con una buena imagen ... Los sitios web de StackExchange tienen un gran ejemplo, este es mi favorito: https://serverfault.com/error