How to see what the user was doing when the error occurred

Does this sound familiar…

User: Your app just crashed
You: What happened?
User: It crashed
You: (sigh) What were you doing at the time?
User: Using the app
You: (sigh) What specifically were you doing?
User: I was clicking in that box
You: Which box?
User: You know, the one on the window you get when you click the other button
…and so it goes on

At one stage, we thought we’d be clever, and when an error happened, we’d pop up a window with a textbox, and we’d ask them to fill in the details. This would then get emailed to us.

Ha ha. If they bothered filling anything in (which most didn’t, they just closed the window and rang us up, which resulted in a conversation much like the above), then it wasn’t much more helpful than before.

We tried pre-populating the textbox with some pointed questions, like “Which window did you have open at the time?” and so on, but the results weren’t any better.

At this stage, we had a brainwave. Let’s not bother asking the user anything. Let’s just email an error report, which would give us as much information as were getting before, but without the frustration. However, this still didn’t get us the extra information we needed to investigate.

We then decided to see if we could get a snapshot of what was on the user’s screen(s) at the time. This would enable us to see exactly where they were, and would hopefully give us more of a clue what went wrong.

One of the other developers went off and spent some time writing some very complex C# that made calls to the Win32 API, etc. It was fairly horrible. Certain that this could be done more easily, I did a bit of research of my own, and came up with the following helper class…

  public static class ScreenshotHelper {
    public static byte[][] GetScreenShots() {
      return Screen.AllScreens
        .Select(screen => screen.GetScreenBytes()).ToArray();
    }

    private static byte[] GetScreenBytes(this Screen screen) {
      Bitmap bmpScreenshot = new Bitmap(screen.Bounds.Width,
                                        screen.Bounds.Height,
                                        PixelFormat.Format32bppArgb);
      Graphics gfxScreenshot = Graphics.FromImage(bmpScreenshot);
      gfxScreenshot.CopyFromScreen(screen.Bounds.X,
                                   screen.Bounds.Y,
                                   0, 0, screen.Bounds.Size,
                                   CopyPixelOperation.SourceCopy);
      byte[] bytes = new byte[0];
      using (MemoryStream ms = new MemoryStream()) {
        bmpScreenshot.Save(ms, ImageFormat.Jpeg);
        ms.Close();
        bytes = ms.ToArray();
      }
      return bytes;
    }
  }

This gives you an array of byte arrays, each of which contains the bits for one screenshot. To enable us to email these, I added another helper class…

  public class EmailAttachment {
    public EmailAttachment(string name, byte[] data) {
      Name = name;
      Data = data;
    }

    public string Name { get; set; }
    public byte[] Data { get; set; }
  }

We could then create these from the byte arrays quite simply…

var Attachments = screenshots
      .Select((s, i) => new EmailAttachment("Screenshot " + i + ".jpg", s))
      .ToList();

…where screenshots is the array of byte arrays.

Attaching these to an email turned out to be trickier than I expected. The end result was a little messy, but works fine. In the class that sends the email, I added three private methods…

private IEnumerable<MemoryStream> AddAttachmentsToEmail(
                                    IEnumerable<EmailAttachment> attachments,
                                    MailMessage msg) {
  List<MemoryStream> streams = new List<MemoryStream>();
  if (attachments != null) {
    foreach (EmailAttachment attachment in attachments
                                             .Where(a => a != null)) {
      MemoryStream ms = new MemoryStream();
      streams.Add(ms);
      byte[] contentBytes = attachment.Data;
      ms.Write(contentBytes, 0, contentBytes.Length);
      ms.Seek(0, SeekOrigin.Begin);
      ContentType ct = GetContentTypeForFile(attachment.Name);
      Attachment a = new Attachment(ms, ct);
      msg.Attachments.Add(a);
    }
  }
  return streams;
}

public static ContentType GetContentTypeForFile(string fileName) {
  switch (Path.GetExtension(fileName)) {
    case ".jpg":
      return new ContentType {
        MediaType = MediaTypeNames.Image.Jpeg,
        Name = fileName
      };
    // other cases here...
    default:
      return new ContentType {
        MediaType = MediaTypeNames.Application.Octet,
        Name = fileName
      };
  }
}

private void CloseMemoryStreams(IEnumerable<MemoryStream> streams) {
  foreach (MemoryStream ms in streams) {
    ms.Close();
  }
}

I could then attach the screenshots to the email and send it as follows…

SmtpClient client = new SmtpClient(...);
MailMessage msg = new MailMessage(...);
IEnumerable<MemoryStream> streams =
  AddAttachmentsToEmail(emailParameters.Attachments, msg);
client.Send(msg);
CloseMemoryStreams(streams);

The messy part was that I needed to keep the streams open until the email had been sent. I’m sure there is a neater way to handle this, but I couldn’t think of one at the time.

The end result was that if an error occurred, we got an email that included information about the user’s PC (which saved us more than once when we found out that the errors were a result of the PC being almost out of memory), as well a copy of the log and these attachments. All of this without having to extract anything from the user.

Be First to Comment

Leave a Reply

Your email address will not be published.

This site uses Akismet to reduce spam. Learn how your comment data is processed.