Using Google Analytics Measurement Protocol with C++/Qt – A Software Engineering Exercise – Part 3

By | 19/03/2014

So far in this series I have only talked about the rudimentary infrastructure I used for setting up a continuous integration loop and a very specific problem to resolve before I could start test driven development. In this part, however, I want to share some more insight about the actual programming process so far. Note that while I will provide code samples, then I am going to omit accompanying test cases, mostly because I am not going to use.

When you look at the protocol’s documentation you’ll quickly realize that it is nothing more than a url-encoded key value list that is being submitted to a specific webserver. This means we could simply use QNetworkRequest instances everywhere we want to track something, for example like this:

QNetworkAccessManager g_nam;
void foo()
{
    QNetworkRequest req;
    req.setUrl("http://www.google-analytics.com/collect");
    QString trackerQuery("v=1&cid=555&tid=UA-12345-678&t=event&ec=test&ev=1&ea=event1");
    QByteArray data = trackerQuery.toLatin1();
    g_nam.post(req, data);
}
// ...
void bar()
{
    QNetworkRequest req;
    req.setUrl("http://www.google-analytics.com/collect");
    QString trackerQuery("v=1&cid=555&tid=UA-12345-679&t=event&ec=test&ea=event2");
    QByteArray data = trackerQuery.toLatin1();
    g_nam.post(req, data);
}

Now, as one can see there is a lot of duplication in this. The first obvious step here would be to take the trackerQuery string out of it:

QNetworkAccessManager g_nam;
void track(const QString& query)
{
    QNetworkRequest req;
    req.setUrl("http://www.google-analytics.com/collect");
    QByteArray data = query.toLatin1();
    g_nam.post(req, data);

}
void foo()
{
    track(QString("v=1&cid=555&tid=UA-12345-678&t=event&ec=test&ev=1&ea=event1"));
}
void bar()
{
    track(QString("v=1&cid=555&tid=UA-12345-679&t=event&ec=test&ea=event2"));
}

This already avoids a lot of the code duplication, but still leaves us with a lot to be desired. For example, the QNetworkAccessManager instance is currently global, which means there is some shared state between the track() method and the rest of your application. Additionally we don’t even know if the request was sent at all because we are not listening to any of QNetworkAccessManager’s signals that would provide us with that information. Thirdly, the string we are passing into track() contains a lot of redundant information and there is no validation of whether it actually is valid according to the API’s or our requirements – for instance it is difficult to spot the typo that I made in bar()’s tracker ID. This particular issue could turn into a very difficult to debug problem that you probably won’t notice until the person who asked for that statistic to be collected comes asking why it does not show up with anything.
So, let’s tackle these problems one by one. Or in this case, let’s just fix the first two problems in one go, really. For that we wrap the track method in a QObject-derived class, which also allows us to connect to the QNetworkAccessManager’s signals.

class Tracker : public QObject
{
    Q_OBJECT
public:
    explicit Tracker( QObject* parent = nullptr ) : QObject(parent), m_nam(new QNetworkAccessManager(this))
    {
        connect(m_nam, SIGNAL(finished(QNetworkReply*)), this, SLOT(onFinished(QNetworkReply*)));
    }
    void track( const QString& data )
    {
        if ( ! m_nam )
        {
            qCritical("No network manager specified!");
        }
        QNetworkRequest req;
        req.setUrl("http://www.google-analytics.com/collect");
        QByteArray data = query.toLatin1();
        m_nam->post(req, data);
    }
    void setNetworkAccessManager(QNetworkAccessManager* nam)
    {
        if (m_nam && m_nam->parent() == this)
        {
            delete m_nam;
        }
        m_nam = nam;
    }
public slots:
    void onFinished(QNetworkReply* reply)
    {
        reply->deleteLater();
        if (reply->error() != QNetworkReply::NoError)
            qCritical("Google Analytics Tracking failed with error: %s", reply->errorString());
    }
private:
    QNetworkAccessManager* m_nam;
};

Well then, this looks a lot better already! Now we are at least getting a message on stderr when our request fails for network and HTTP protocol reasons. We have also managed to encapsulate QNetworkAccessManager’s access if required. Be aware, though, that we are not just missing the actual payload validation, we have also introduced a defect if we share QNetworkAccessManager instances. I leave the fix for it as an exercise to the reader (I myself have just fixed this in the git repository today), but I will hint at the fact that one signal can have multiple slots connected to it, which should make the error very obvious.
The next step would now be to remove error potential from the data string we are passing in. For this I opted to turn some of the required parameters into member variables on the tracker class, and keep track of the others with a QList> that I pass into a QUrlQuery object for correct URL-encoding.

However, I think that I have written enough for this installment, and instead will focus my remaining spare time tonight on actually working on the library. The next part of this series will deal with the specifics of the query data and how to validate it so that we can avoid query errors like the one mentioned above. Until then feel free to leave me questions, criticisms and thoughts in the comments.

Leave a Reply

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