Building a Custom Defect Submitter for qTrace

One of the little-known secrets of qTrace is its Tracker API which allows developers to extend qTrace and have its defect reports submitted to a custom defect tracker.  It’s worth noticing that while the API documentation refers to these outlets as “defect trackers”, it doesn’t necessarily require that these have to be exactly some defect tracking products like Jira, Bugzilla and so on.  In fact, a custom outlet can be anything that you want to send the qTrace defect reports to, be it a web server, a FTP server or a cloud storage etc.  This article demonstrates qTrace Tracker API by going through the steps to build a custom FTP submitter for qTrace.

Create a VS.NET Project

Open Visual Studio and create a library project.  Name it FtpSubmitter.

Add references to the following assemblies:

  • qTrace.Trackers.Contracts: this is shipped with qTrace and can be found in qTrace installation folder.  This assembly contains the core API that the submitter will build upon.
  • Other assemblies shipped with .NET 4.0:
    • PresentationCore
    • PresentationFramework
    • WindowBase
    • System.Xaml
    • System.ComponentModel.Composition

Before we complete this setup step, let’s do one more thing that can save time later.  Define a post-build action which copies the output of the project to [qTrace Installation Folder]/Trackers/.  This is the folder where qTrace looks for custom submitters at runtime.  By defining this build action, we can test our submitter during the development period  simply by launching qTrace.

Create Submitter Class

Create a class named Ftp. This is the only class we need to build the submitter. It must IBugTracker interface, defined in qTrace.Trackers.Contracts.  Besides, make sure you annotate it with [Export(typeof(IBugTracker))] so that it can be discovered by qTrace at runtime. The Export attribute comes from the System.ComponentModel.Composition namespace. The empty implementation for this class looks like below.

namespace FtpSubmitter
{
    using System;
    using System.Collections.Generic;
    using System.ComponentModel.Composition;
    using System.Windows;
    using Iris.Trackers.Contracts;

    [Export(typeof(IBugTracker))]
    public class Ftp : IBugTracker
    {
        public string DisplayName
        {
            get { throw new NotImplementedException(); }
        }

        public string IconUri
        {
            get { throw new NotImplementedException(); }
        }

        public string Verify(BugTrackerAccount bugTrackerAccount, int timeoutInMillis, IDictionary settings)
        {
            throw new NotImplementedException();
        }

        public SubmittedDefect Submit(Window owner, BugTrackerAccount bugTrackerAccount, int timeoutInMillis, Defect defect, Func> attachmentsFunc, IDictionary settings)
        {
            throw new NotImplementedException();
        }
    }
}

Note that DisplayNameIconUri, Verify(), and Submit() are all defined in IBugTracker. I summary their purposes below. For more information, you should refer to the API documentation.

  • DisplayName: the name of the submitter as will be seen by users
  • IconUri: the icon to display within qTrace
  • Verify(): verify a URL and authenticate the provided user name and password
  • Submit(): submit qTrace output(s)

Let’s implement the 2 properties first.

public string DisplayName
{
    get { return "FTP Submitter"; }
}

public string IconUri
{
    get { return "/FtpSubmitter;component/ftp.png"; }
}

Note that ftp.png is an icon image I want the submitter to have. It’s located in the root folder of the project and included as Embedded Resource.  The URI format is a convention in WPF and constructed by appending /FtpSubmitter;component/ (FtpSubmitter is the assembly name) to the relative path of the file. You can also opt to return null if there’s no icon.

Okay, let’s test our submitter. Build the project and then launch qTrace. (Remember that we defined a build action to copy our assembly to the expected folder, so qTrace should have no problem picking it up.) Open the Settings screen, you should see our submitter displayed in the Defect Trackers tab.

Go ahead filling in some valid FTP URL, user name and password and click Save. qTrace will save these settings and associate them with the submitter. Currently qTrace only allows these 3 pieces of information to be filled in the Settings screen. If you need more, you can have users input additional information in the submission screen which we’ll talk more later in this article. This is one area where the API could be improved.

Note that the submitter doesn’t do anything yet and clicking Test Connection will cause qTrace to display an error message. What does Test Connection do? Basically it will invoke the Verify() method of the selected submitter, passing in the provided URL, user name and password. Let’s implement the method.

public string Verify(BugTrackerAccount bugTrackerAccount,
                        int timeoutInMillis,
                        IDictionary settings)
{
    var ftpRequest = (FtpWebRequest)WebRequest.Create(bugTrackerAccount.Url);
    ftpRequest.Credentials = new NetworkCredential(bugTrackerAccount.UserName,
                                                    bugTrackerAccount.Password);
    ftpRequest.Timeout = timeoutInMillis;
    ftpRequest.Method = WebRequestMethods.Ftp.ListDirectory;
    using (ftpRequest.GetResponse())
    {
        return "Connected to FTP server successfully!";
    }
}

Basically the method creates an FtpWebRequest object, fills in connection and credentials information and attempts to send a ListDirectory method. If you input valid information, the success message will be returned. You can also return null if you want qTrace to show the default success message instead. If there is any exception, qTrace will display it to the end-users.

Note that the timeoutInMillis argument is provided by qTrace, not configured by users in the Settings screen. Currently qTrace doesn’t enforce the timeout, but it’s a good idea to adhere to this timeout unless you have good reason otherwise. After all, you don’t know if a future release of qTrace enforces this or allows users to configure a timeout, right?

There’s also the settings argument which you can use to store anything you want. This is the storage area for each submitter (shared by both Verify() and Submit() and will eventually be persisted to file. Therefore, you can be sure that it will always be available.

Finally, it’s worth noting that this method is invoked from a background thread, so make sure you don’t do UI related tasks here without first marshaling them to the UI thread.

Build the project and launch qTrace again. Go to the Settings screen, make sure you have valid connection information and click Test Connection. Your screen should look like the one below.

Next, let’s implement the Submit() method. When is this method invoked? When users click on the Submit button in the qTrace editor.

All built-in defect tracker submitters in qTrace (e.g. JIRA, Assembla, Rally, FogBugz etc.) implement this method by first connecting to the tracker server, querying metadata (e.g. projects, components, priorities etc.) and displays a window for users. Users can then choose to show/hide fields, set default values, and input values for the fields before actually having the defect report sent to the tracker. It’s our assumption that most custom defect tracker submitters will carry out similar steps. Therefore, it shouldn’t come as a surprise if this method is always invoked in the UI thread, so that one does not have to do a lot of marshaling between the background and UI threads.

The FTP Submitter, should it be more properly implemented, should do something similar, i.e. allowing users to choose folder to save to, configure Active/Passive mode, change file name etc. However, for the purpose of this article, let’s keep things simple and go with the windowless approach: the submitter will simply send the defects to whatever folder specified in the URL (in the Settings screen). I do want to add a little bit of sophistication though: since it’s possible for users to choose to export a trace to multiple images, the code will spawn multiple threads to upload using the Task Parallel Library.

public SubmittedDefect Submit(Window owner,
                                BugTrackerAccount bugTrackerAccount,
                                int timeoutInMillis,
                                Defect defect,
                                Func> attachmentsFunc,
                                IDictionary settings)
{
    Mouse.OverrideCursor = Cursors.Wait;
    try
    {
        var tasks = from attachment in attachmentsFunc()
                    select Task.Factory.StartNew(
                        () => Upload(bugTrackerAccount, timeoutInMillis,
                                        attachment.Key, attachment.Value));
        Task.WaitAll(tasks.ToArray());
        return null;
    }
    finally
    {
        Mouse.OverrideCursor = null;
    }
}

private void Upload(BugTrackerAccount bugTrackerAccount,
                    int timeoutInMillis,
                    string filePath,
                    byte[] fileContent)
{
    var uri = bugTrackerAccount.Url +
                (bugTrackerAccount.Url.EndsWith("/") ? string.Empty : "/") +
                Path.GetFileName(filePath);
    var ftpRequest = (FtpWebRequest)WebRequest.Create(uri);
    ftpRequest.Credentials = new NetworkCredential(bugTrackerAccount.UserName,
                                                    bugTrackerAccount.Password);
    ftpRequest.Timeout = timeoutInMillis;
    ftpRequest.Method = WebRequestMethods.Ftp.UploadFile;
    using (Stream writer = ftpRequest.GetRequestStream())
    {
        writer.Write(fileContent, 0, fileContent.Length);
    }
}

Let’s dissect this implementation. Upload() is a helper method which uses FtpWebRequest to upload a file. Submit() generates the files (by invoking the attachmentFunc() thunk provided by the API), spawns multiple threads, one for each file, and waits until all the threads finish. Note that the cursor is changed to waiting state so that users know the submission is being done. In general, it is recommended that a progress window is shown to users, like the way built-in trackers in qTrace do, not just changing the application cursor. Normally, this method should return a SubmittedDefect object which has the ID and URL of the defect. That makes sense for the “real” defect trackers, but not for our FTP submitter. In this case, I return null and qTrace will do nothing after the submission. Admittedly, this appears like a hack and could be improved in future release of the API. Finally, if there’s any exception thrown in the method, qTrace will display an error message to users.

Build the project and launch qTrace again. Now, try to capture some screenshots with qTrace then click Submit. If nothing goes wrong, qTrace output(s) should be uploaded to your FTP folder! That’s it, everyone, we’ve just built a working custom defect submitter for qTrace.

The full implementation of the class is shown below. You can also download the whole VS.NET solution from GitHub. I hope the information provided in this article will be useful for you. If you have any question or suggestion about the qTrace Tracker API, please let me know.

namespace FtpSubmitter
{
    using System;
    using System.Collections.Generic;
    using System.ComponentModel.Composition;
    using System.IO;
    using System.Linq;
    using System.Net;
    using System.Threading.Tasks;
    using System.Windows;
    using System.Windows.Input;
    using Iris.Trackers.Contracts;

    [Export(typeof(IBugTracker))]
    public class Ftp : IBugTracker
    {
        public string DisplayName
        {
            get { return "FTP Submitter"; }
        }

        public string IconUri
        {
            get { return "/FtpSubmitter;component/ftp.png"; }
        }

        public string Verify(BugTrackerAccount bugTrackerAccount,
                             int timeoutInMillis,
                             IDictionary settings)
        {
            var ftpRequest = (FtpWebRequest)WebRequest.Create(bugTrackerAccount.Url);
            ftpRequest.Credentials = new NetworkCredential(bugTrackerAccount.UserName,
                                                           bugTrackerAccount.Password);
            ftpRequest.Timeout = timeoutInMillis;
            ftpRequest.Method = WebRequestMethods.Ftp.ListDirectory;
            using (ftpRequest.GetResponse())
            {
                return "Connected to FTP server successfully!";
            }
        }

        public SubmittedDefect Submit(Window owner,
                                      BugTrackerAccount bugTrackerAccount,
                                      int timeoutInMillis,
                                      Defect defect,
                                      Func> attachmentsFunc,
                                      IDictionary settings)
        {
            Mouse.OverrideCursor = Cursors.Wait;
            try
            {
                var tasks = from attachment in attachmentsFunc()
                            select Task.Factory.StartNew(
                                () => Upload(bugTrackerAccount, timeoutInMillis,
                                             attachment.Key, attachment.Value));
                Task.WaitAll(tasks.ToArray());
                return null;
            }
            finally
            {
                Mouse.OverrideCursor = null;
            }
        }

        private void Upload(BugTrackerAccount bugTrackerAccount,
                            int timeoutInMillis,
                            string filePath,
                            byte[] fileContent)
        {
            var uri = bugTrackerAccount.Url +
                      (bugTrackerAccount.Url.EndsWith("/") ? string.Empty : "/") +
                      Path.GetFileName(filePath);
            var ftpRequest = (FtpWebRequest)WebRequest.Create(uri);
            ftpRequest.Credentials = new NetworkCredential(bugTrackerAccount.UserName,
                                                           bugTrackerAccount.Password);
            ftpRequest.Timeout = timeoutInMillis;
            ftpRequest.Method = WebRequestMethods.Ftp.UploadFile;
            using (Stream writer = ftpRequest.GetRequestStream())
            {
                writer.Write(fileContent, 0, fileContent.Length);
            }
        }
    }
}

7 comments on Building a Custom Defect Submitter for qTrace

  1. Hello!
    Is it possible to create additional fields in Defect Trackers settings dialog? To specify project name, for example?

  2. Buu Nguyen says:

    @Vadim:
    Additional fields should be shown in a dialog by Submit() (that’s why this method is always invoked on UI thread for convenience). Currently that’s how the built-in tracker adapters are built. The reason is that users should be able to modify those fields before submitting a defect. As for the Settings screen, it’s fixed with only connection information. Feel free to let me know if you have any further question.

  3. Teri says:

    Hi there, I enjoy reading all of your post.
    I like to write a little comment to support you.

  4. Henry says:

    Wow, awesome blog layout! How long have you been blogging for?
    you make blogging look easy. The overall look of your site is magnificent,
    let alone the content!

  5. Everett says:

    It is perfect time to make some plans for the future and it is time
    to be happy. I’ve read this post and if I could I desire to suggest you some interesting things or tips. Maybe you could write next articles referring to this article. I desire to read even more things about it!

  6. Laurence says:

    Hi, i read your blog occasionally and i own a similar one
    and i was just wondering if you get a lot of spam remarks?
    If so how do you reduce it, any plugin or anything you can suggest?
    I get so much lately it’s driving me crazy so any assistance is very much appreciated.

  7. Nelly says:

    There are articles that ought to be raised to manifesto status.
    Your article is one of those. The points are spot on!

    Encouragement, insight, and inspiration are
    sound tools to exhibit authority in blog posts.
    This authority leads to career enhancing opportunities, building friends, and
    strong brand building. Thanks so much for bringing this
    out into the forefront. It has been extremely helpful to me.

Leave a Reply

Your email address will not be published. Required fields are marked *

More Great Content

Get Started with QASymphony