OK, it’s been a couple of days since the last post. This is what happens when you have to prioritize some other things - like brewing beer - in between. But now we’re back on track with our small library, and this time we focus on hit parameter validation.

There are some different ways of attempting parameter validation, and all of them have their own pros and cons. Since the tracker already takes care of a minimum set of required parameters, then the first idea is to do something along the lines of this:

  • Determine the hit type - it’s a required parameter
  • Check if all required parameters for this hit type exist
  • For all specified parameters: Check if their name is known, and if so, if their value matches expected values.

This would do the job, and provide a great first iteration target. And even though it imposes run-time overhead then we really shouldn’t worry about it yet. In fact, since QUrlQuery does not use a QHash or QMap for its values then the first step is to convert our list of string pairs into a more suitable data structure. Once we are done converting we can easily do key look-ups as necessary:

// ...
        QHash<QString, QString> params;
        QListIterator<QPair<QString, QString>> iter( parameters );
        while ( iter.hasNext() )
        {
            auto param = iter.next();
            params.insert( param.first, param.second );
        }
// ...

Now that that’s done we can go and start with writing some tests first, just to make sure that whatever we come up with actually does what we want.

Given the protocol then I am sure that the following tests should be adequate:

  • Check that a valid hit type was specified.
  • For each of the hit types with required parameters, detect if they are missing a required parameter.
  • For each parameter value type, detect if it can be detected correctly, e.g. cannot pass text to something that expects an int.

Or, in code:

TEST(Validation, hitTypeTests)
{
    Tracker::ParameterList params;
    // 1. hit type is required
    EXPECT_FALSE( isValidHit( params ) );
    // 2. Must be one of 'pageview', 'appview', 'event', 'transaction', 'item', 'social', 'exception', 'timing'.
    params << QPair<QString, QString>( "t", "foo" );
    EXPECT_FALSE( isValidHit( params ) );
    // 3. Make sure we succeed on correct parameter
    params.clear();
    params << QPair<QString, QString>( "t", "pageview" );
    EXPECT_TRUE( isValidHit( params ) );
}

TEST(Validation, requiredParameterTests)
{
    Tracker::ParameterList params;
    // 1. check for a hit type that has no required parameters
    params << QPair<QString, QString>( "t", "pageview" );
    EXPECT_TRUE( isValidHit( params ) );
    // 2. detect parameters are missing
    params.clear();
    params << QPair<QString, QString>( "t", "item" );
    EXPECT_FALSE( isValidHit( params ) );
}

TEST(Validation, correctParameterTypeTests)
{
    Tracker::ParameterList params, baseParams;
    baseParams << QPair<QString, QString>( "t", "event" );

    // 1. Boolean, valid
    params = baseParams;
    params << QPair<QString, QString>( "aip", "0" );
    params << QPair<QString, QString>( "je", "1" );
    EXPECT_TRUE( isValidHit( params ) );
    // 2. Boolean, invalid
    params = baseParams;
    params << QPair<QString, QString>( "aip", "foo" );
    EXPECT_FALSE( isValidHit( params ) );

    // 3. Currency, valid
    params = baseParams;
    params << QPair<QString, QString>( "tr", "-55.00" );
    params << QPair<QString, QString>( "ts", "1000.000001" );
    EXPECT_TRUE( isValidHit( params ) );
    // 4. Currency, invalid
    params = baseParams;
    params << QPair<QString, QString>( "tt", "foo" );
    EXPECT_FALSE( isValidHit( params ) );

    // 5. Integer, valid
    params = baseParams;
    params << QPair<QString, QString>( "utt", "-1234567890" );
    params << QPair<QString, QString>( "iq", "9876543210" );
    params << QPair<QString, QString>( "ev", "0" );
    EXPECT_TRUE( isValidHit( params ) );
    // 6. Integer, invalid
    params = baseParams;
    params << QPair<QString, QString>( "plt", "foo" );
    EXPECT_FALSE( isValidHit( params ) );
}

Great! Now it should be fairly trivial to implement a validation routine that satisfies all our tests. First of all, to ensure that a valid hit type is specified we simply need to find the parameter with key “t” and see if its value is in the set of hit types (“pageview”, “event”, “transaction”, etc). This can be achieved with a simple look-up table (or list, in this case):

// ... construct a QHash<QString, QString> params
        QList<qstring> validHitTypes;
        validHitTypes << "pageview" << "appview" << "event" << "transaction";
        validHitTypes << "item" << "social" << "exception" << "timing";

        bool foundHitType = false;
        auto hitIter = params.find( "t" );
        if ( hitIter != params.end() && validHitTypes.contains( hitIter.value() ) )
        {
            foundHitType = true;
            // ... more validation
        }
// ... etc

And now that the first test is passing we can apply similar functionality for required parameters. Let’s do this after we found the hit type, because we really only need to do it at that point:

// ... another lookup "table"
        QMap<QString, QStringList> requiredPerHitType;
        requiredPerHitType.insert( "transaction", QStringList() << "ti" );
        requiredPerHitType.insert( "item", QStringList() << "ti" << "in" );
        requiredPerHitType.insert( "social", QStringList() << "sn" << "sa" << "st" );
// ...
        bool hasAllRequiredParameters = true;
        bool foundHitType = false;
        auto hitIter = params.find( "t" );
        if ( hitIter != params.end() && validHitTypes.contains( hitIter.value() ) )
        {
            foundHitType = true;

            auto requiredIter = requiredPerHitType.find( hitIter.value() );
            if ( requiredIter != requiredPerHitType.end() )
            {
                QListIterator<qstring> iter( requiredIter.value() );
                while ( iter.hasNext() )
                {
                    auto value = iter.next();
                    hasAllRequiredParameters = hasAllRequiredParameters && params.keys().contains( value );
                }
            }
        }
// ...

And thus the second set of tests passes as well. Did I already mention that look-up tables are a great thing? Well, now I did. So we are now left with validating the type of the actual parameters. There really is not much to do for the text type because Google more or less accepts everything that’s correctly URL-encoded, as long as it’s shorter than the allowed maximum size. Boolean values are also simple to validate because they must be either “0” (=false) or “1” (=true). Leaving us with Integer, which is a signed 64-bit integer value, and currency. Qt makes validating the 64 bit integer really easy, we simply convert the textual value to a numeric value and back again before we compare both:

boolean isValidInteger(const QString& value)
{
    return ( value == QString::number( value.toLongLong() ) );
}

Validating the currency type is a bit more weird and slightly unclear. Google’s documentation says:

Used to represent the total value of a currency. A decimal point is used as a delimiter between the whole and fractional portion of the currency. The precision is up to 6 decimal places. […] Once the value is sent to Google Analytics, all text is removed up until the first digit, the - character or the . (decimal) character

Now, some people, when confronted with a problem instantly go and say: “I know, I can use regular expressions!”. And while they end up with 2 problems most of the time, then it is a completely feasible solution here. As far as I am aware then this is a suitable, non-escaped regular expression, e.g. you’ll have to escape the backslashes if you use it in your C++ code:

.*-?\d*\.\d{2,6}

In more readable terms, the expression accepts whatever text is in front of a potential sign, then expects all digits up until it encounters a decimal point, at which point (sic) it then expects another 2 to 6 decimal digits.

So, now that we have basic validation running (except for maximum length of single parameters), we can declare the library as ready to use. In the next part I’m going to reflect a little bit on the experiences gained during the development of this utility, and give a little outlook on where I see room for potential improvements (and there is some room for that) that would make this library better on a code quality level.