Collecting app feedback with Codebase issues

Written by Emil Loer on Feb 15, 2016 |

An important part of app development is listening to your users and learn what they think of your app. There are a couple of methods you can do this. You can wait until people review your app in the App Store, but without an incentive they will most likely only do this when something is wrong. The same goes for contact information on your website. Of course you can instruct your users to go and review your app and leave suggestions or send some e-mails but this requires the user to leave the app. Not a great experience, I think, because of the many steps involved.

There is a better way. For an app I'm currently developing I'm building a feedback form inside the app. This allows me to ask my users what they think of the app while staying in the app. Furthermore, I can ask them the exact questions that I need to know to help them.

To accomplish this I'm trying to make use of what I already have. In this case, I will use the ticketing system inside Codebase because I already use it for many other things.

Getting access

Codebase uses an XML over HTTP API that you can leverage for your own projects. In order to gain access and create tickets you need to have a project, a user and the user's API credentials. You can find the API credentials for the current user under the "My Profile" section of Codebase. For the project you need the permalink name, this can be found in the project settings and is usually the project name in lowercase with spaces replaced by hyphens.

Let's put all of these in some constants first:

let project = "your-codebase-project"
let user = "mycompany/myusername"
let apiKey = "your Codebase API key here"

Collecting information

In order to create a ticket we need to tell Codebase what is actually in the ticket. In the most simple case a ticket consists of a summary and a description. The summary can be interpreted as the ticket's title so that's how we will use it here. Let's put this inside a Swift struct:

/// A Codebase feedback ticket
struct CodebaseTicket {
    /// The ticket's summary (title). 
    var summary: String

    /// The ticket's description string. This can consist of multiple lines.
    var description: String
}

Now because Codebase uses XML over HTTP we have to convert this ticket to an XML representation. If you're on Mac OS X you can use NSXMLDocument for this, but on iOS this class is not available so we'll have to resort back to string concatenation. But I'll give you both options!

#if os(OSX)
    // Construct an XML document for the ticket
    let ticket = NSXMLElement(name: "ticket")
    ticket.addChild(NSXMLElement(name: "summary", stringValue: summary))
    ticket.addChild(NSXMLElement(name: "description", stringValue: description))

    let body = NSXMLDocument(rootElement: ticket).XMLData
#else
    let body = "<ticket>" +
        "<summary><![CDATA[\(summary)]]></summary>" +
        "<description><![CDATA[\(description)]]></description>" +
        "</ticket>".dataUsingEncoding(NSUTF8StringEncoding)
#endif

Now body contains an NSData containing the XML document, which will be used in the next section.

Submitting the ticket

For submitting the ticket to Codebase we can use NSURLSession. There is a catch however, because Codebase wants us to authenticate to the API using HTTP Basic Authentication. This normally consists of two steps: waiting for a WWW-Authenticate header from the server and sending a second request using an Authorization header. Unfortunately, waiting for the authenticate header using NSURLSession requires setting up a delegate and writing a lot of boilerplate code. But there is a simpler way: we can just set the authorization header directly in our URL request.

Here's how to do it:

// Create the URL request
let request = NSMutableURLRequest(URL: NSURL(string: url)!)
request.HTTPMethod = "POST"
request.HTTPBody = body
request.addValue("application/xml", forHTTPHeaderField: "Accept")
request.addValue("application/xml", forHTTPHeaderField: "Content-Type")

// Set the authorization header
let credentials = "\(user):\(apiKey)"
let auth = "Basic " + credentials.dataUsingEncoding(NSUTF8StringEncoding)!.base64EncodedStringWithOptions([])
request.addValue(auth, forHTTPHeaderField: "Authorization")

Now we just have to issue the request and do something with the result inside the request callback.

// Issue the request
let task = NSURLSession.sharedSession().dataTaskWithRequest(request) { data, response, error in
    // Do something with the result here
}

task.resume()

Using the tickets

The only thing that remains is actually using the ticket struct and integrating it in your app. The code below can be used in your favourite view controller.

let ticket = CodebaseTicket(
    summary: "My feedback",
    description: "I think your app is awesome!"
)

ticket.postWithCallback { error in
    if let error = error {
        // Handle the error
    } else {
        // Everything went ok
    }
}

Complete code example

The complete code for the Codebase ticket creator is given below.

import Foundation

/// A Codebase feedback ticket
struct CodebaseTicket {
    let project = "your-codebase-project"
    let user = "mycompany/myusername"
    let apiKey = "your Codebase API key here"

    /// The ticket's summary (title). 
    var summary: String

    /// The ticket's description string. This can consist of multiple lines.
    var description: String

    /// Send the ticket to Codebase. Calls the callback on completion with an
    /// NSError instance when something went wrong, nil otherwise.
    func postWithCallback(callback: NSError? -> Void) {
        let url = "https://api3.codebasehq.com/\(project)/tickets"

        #if os(OSX)
            // Construct an XML document for the ticket
            let ticket = NSXMLElement(name: "ticket")
            ticket.addChild(NSXMLElement(name: "summary", stringValue: summary))
            ticket.addChild(NSXMLElement(name: "description", stringValue: description))

            let body = NSXMLDocument(rootElement: ticket).XMLData
        #else
            let body = "<ticket>" +
                "<summary><![CDATA[\(summary)]]></summary>" +
                "<description><![CDATA[\(description)]]></description>" +
                "</ticket>".dataUsingEncoding(NSUTF8StringEncoding)
        #endif

        // Create the URL request
        let request = NSMutableURLRequest(URL: NSURL(string: url)!)
        request.HTTPMethod = "POST"
        request.HTTPBody = body
        request.addValue("application/xml", forHTTPHeaderField: "Accept")
        request.addValue("application/xml", forHTTPHeaderField: "Content-Type")
        // Set the authorization header
        let credentials = "\(user):\(apiKey)"
        let auth = "Basic " + credentials.dataUsingEncoding(NSUTF8StringEncoding)!.base64EncodedStringWithOptions([])
        request.addValue(auth, forHTTPHeaderField: "Authorization")

        // Issue the request
        let task = NSURLSession.sharedSession().dataTaskWithRequest(request) { data, response, error in
            callback(error)
        }

        task.resume()
    }
}

Final thoughts

Of course this is just the beginning. Codebase understands many more properties like priorities, categories and tags. The framework I provided can easily be extended to supply whatever information you need to the API. For more information about the possibilities see the Codebase API documentation.

If you've enjoyed this post please follow me on Twitter or Facebook. I'd really appreciate it!