Iris Classon
Iris Classon - In Love with Code

Using the StatCounter API (C# example Net and WinRT)

As far as I know StatCounter- the tracker I use for my visitors numbers does not have a Windows Phone application, but even if they did I would like to do some custom things to aggregate data and do some calculations. StatCounter has an API which is accessible for paying customers (like me, and it’s quite affordable). On my Kanban board at home I had had for a while a card in my “I’d like to create/try”-section a card for a StatCounter application. While in flight I hacked together a crappy little demo app for fun. I thought it might be of interest for those searching the interwebs looking for an example.

A tiny side note

The demo uses code behind as the focus of this post is on how the ‘stats’ part of the API works, demonstrated with both WinRT and .Net as the only difference is the SHA1 hashing. Don’t give me a hard time for the code behind, you know I would totally tattoo MVVM in a heart and a barbwire on my arm. #Hello90’s

Make sure you are set up

You need to have a paid account.

You need to set a password. Notice that as long as you have *a* username- be that yours or not, you can set the password.

The full documentation can be found here

Querying the API

Querystrings are used to query the data, this means that the querystring starts with a questionmark and each key value pair is separated by an ampersand.

The query has some mandatory entries:

  • Project ID

  • Username

  • SHA1 hash of url (explained further down)

  • API version (latest is 3)

  • Unix time stamp

Optional:

  • Format. Default is JSON, but you can set format to XML with &f=xml

The API base url is:

http://api.statcounter.com/

And there are different sub sections depending on what you want to do. We want to query the project statistics so we use

http://api.statcounter.com/stats/

Building the query

The query consists of the following parts:

  1. The base URL which is http://api.statcounter.com/stats/

  2. The ‘base’ querystring which contains query details as well as the following of the mandatory data: username, version, unix time stamp, project ID.

  3. The ’tail’ of the URL which is a SHA1 hash created from the base querystring with the password appended to the username

Base URL + Base Querystring + SHA1 hash(querystring+password). Hopefully this diagram helps

The stats API lets us query quite a few things, but in this demo I’ll focus on the summary- which provides page views, unique views, returning visitors and so on.

To make it easier for you to see what the querystring consists of I’ve added the query pairs in a string array, and later glue the thing together in a method that appends a ‘?’’ at the beginning and separates them by ‘&’ using the stringbuilder.

Let’s take a look at the items in the array:

“vn=” + version,

“s=summary”,

“g=” + Granularity.Daily.ToString().ToLower(),

“sd=” + startDatePicker.Date.Day,

“sm=” + startDatePicker.Date.Month,

“sy=” + startDatePicker.Date.Year,

“ed=” + endDatePicker.Date.Day,

“em=” + endDatePicker.Date.Month,

“ey=” + endDatePicker.Date.Year,

“pi=” + projectId.Text,

“t=” + unixTimeStamp,

“u=” + username.Text,

[sourcecode language=“csharp”]
var details = new[]
{
“vn=” + version,
“s=summary”,
“g=” + Granularity.Daily.ToString().ToLower(),
“sd=” + startDatePicker.Date.Day,
“sm=” + startDatePicker.Date.Month,
“sy=” + startDatePicker.Date.Year,
“ed=” + endDatePicker.Date.Day,
“em=” + endDatePicker.Date.Month,
“ey=” + endDatePicker.Date.Year,
“pi=” + projectId.Text,
“t=” + unixTimeStamp,
“u=” + username.Text,
};
[/sourcecode]

First we have version, set to the latest which is 3. We then set type of data to summary. ‘G’ stands for granularity. To show the options I’ve made an enum. Yes I know, some don’t like enums and I’m not a big fan- I prefer strongly typed enum pattern if I have to have that functionality but that would be overkill for this example.

[sourcecode language=“csharp”]
public enum Granularity
{
Hourly,
Daily,
Weekly,
Monthly,
Quarterly,
Yearly
}
[/sourcecode]

After that we need to set start date and end date broken down by day, month and year. As you can see I’ve used a two date pickers here, that defaults to last seven days.

After that I pass in the numeric project ID

Next up is the Unix time stamp, which is the current date minus 1970/1/1 in seconds (total seconds)

[sourcecode language=“csharp”]
//

var unixTimeStamp = (Int32)(DateTime.UtcNow.Subtract(new DateTime(1970, 1, 1))).TotalSeconds;

//
[/sourcecode]

Lastly for the base querystring is the username.

Once we have all that information I created the querystring, which might for example look like this (made up data):

?vn=3&s=summary&g=daily&sd=28&sm=10&sy=2014&ed=4&em=11&ey=2014&pi=1234567&t=1415127884&u=myUserName

We will use that base and add the password to the end of it, not prefixed by anything (simply glued to the username) and create a SHA1 hash out of that (yes, all of it). So based on the last example what we will need to hash is (notice MyPassword at the end):

?vn=3&s=summary&g=daily&sd=28&sm=10&sy=2014&ed=4&em=11&ey=2014&pi=1234567&t=1415127884&u=myUserNameMyPassword

Another diagram to help explain:

In WinRT that would be something along the lines of:

[sourcecode language=“csharp”]
private string GenerateHashedString(string stringToBeHashed)
{
var algorithm = HashAlgorithmProvider.OpenAlgorithm(HashAlgorithmNames.Sha1);
var buffer = CryptographicBuffer.ConvertStringToBinary(stringToBeHashed, BinaryStringEncoding.Utf8);
var hashedData = algorithm.HashData(buffer);
return CryptographicBuffer.EncodeToHexString(hashedData);
}
[/sourcecode]

In .Net maybe something like this:

[sourcecode language=“csharp”]
public string GenerateHashedString(string plainString)
{
var encodedString = Encoding.UTF8.GetBytes(plainString);
var computedHash = SHA1.Create().ComputeHash(encodedString);
const string hexFormatting = “x2”;

        var hashedResult = new StringBuilder();  

        foreach (var computedByte in computedHash)  
        {  
            hashedResult.Append(computedByte.ToString(hexFormatting));  
        }  
        return hashedResult.ToString();  
    }  

[/sourcecode]

Once we have the hash we can piece together the full url.

First we add the base URL, then we add the querystring, then we add the hash like this: &sha1=MYHASH.

End result:

?vn=3&s=summary&g=daily&sd=28&sm=10&sy=2014&ed=4&em=11&ey=2014&pi=1234567&t=1415127884&u=myUserName&sha1=076fe83f015c893ad6f566ce2170d84c54e73bdd

Using the HTTPClient I’ve made a call with just very rudimentary error handling (you will need a reference to System.Net.Http). Since I haven’t set format default will be JSON which I’m quite happy with. Once the data is on your side it’s up to you how you want to deal with it.

What seems to be the default response is JSON structured like this (left column):

For the purpose of this demo I’d like to show you two things, how the JSON looks like, as well as the data shown in a slightly more readable way (notice reversed order of the items to display last entry first) without having to know the structure of the data, besides the root object array. The app displays both side by side. Pretty, isn’t it? (No, not really- but good enough for me :D )

What you do with the code from here on is up to you, don’t hesitate to share any ideas, thoughts or projects that you have :) And oh yes, I still screw up all the time so make sure you correct me if something is outdated, wrong, has alternatives or what not :D

Here is the full, incredibly messy code, :

[sourcecode language=“csharp”]
private async void Button_Click(object sender, RoutedEventArgs e)
{
errorBox.Visibility = Visibility.Collapsed;
var version = 3;
var unixTimeStamp = (Int32)(DateTime.UtcNow.Subtract(new DateTime(1970, 1, 1))).TotalSeconds;

        var details = new[]  
        {  
            "vn=" + version,  
            "s=summary",  
            "g=" + Granularity.Daily.ToString().ToLower(),  
            "sd=" + startDatePicker.Date.Day,  
            "sm=" + startDatePicker.Date.Month,  
            "sy=" + startDatePicker.Date.Year,  
            "ed=" + endDatePicker.Date.Day,  
            "em=" + endDatePicker.Date.Month,  
            "ey=" + endDatePicker.Date.Year,  
            "pi=" + projectId.Text,  
            "t=" + unixTimeStamp,  
            "u=" + username.Text,  
        };  

        var baseUrl = CreateBaseQueryString(details);  
        var sha1Hash = GenerateHashedString(baseUrl + passwordBox.Password);  
        var url = String.Format("{0}&sha1={1}", baseUrl, sha1Hash);  

        using (var client = new HttpClient())  
        {  
            client.BaseAddress = new Uri("http://api.statcounter.com/stats/");  

            try  
            {  
                var response = await client.GetAsync(url);  

                response.EnsureSuccessStatusCode();  

                if (response.IsSuccessStatusCode)  
                {  
                    var res = await response.Content.ReadAsStringAsync();  
                    dynamic obj = JsonConvert.DeserializeObject(res);  
                    JArray items = obj.sc\_data;  

                    result.Text = "";  

                    foreach (var item in items.Children().Reverse())  
                    {  
                        foreach (var property in item.Properties())  
                        {  
                            var cleanedName = property.Name.Replace("\_", " ");  
                            var formattedText = string.Format("{0} : {1}", cleanedName, property.Value);  
                            result.Text += Environment.NewLine + formattedText;  
                        }  
                        result.Text += Environment.NewLine;  
                    }  
                    jsonResult.Text = FormattedJson(res);  
                }  
            }  
            catch (Exception exception)  
            {  
                ShowErrorMessage(exception.Message);  
            }  
        }  
    }  

    void ShowErrorMessage(string errorDetail)  
    {  
        errorBox.Visibility = Visibility.Visible;  
        errorMessage.Text = string.Format("Could not fetch data: {0}", errorDetail);  
    }  

    private static string FormattedJson(string json)  
    {  
        dynamic parsedJson = JsonConvert.DeserializeObject(json);  
        return JsonConvert.SerializeObject(parsedJson, Formatting.Indented);  
    }  

    public static string CreateBaseQueryString(string[] details)  
    {  
        var urlConstruct = new StringBuilder();  

        foreach (var detail in details)  
        {  
            var separator = detail == details[0] ? "?" : "&";  

            urlConstruct.Append(separator);  
            urlConstruct.Append(detail);  
        }  

        return urlConstruct.ToString();  
    }  

    public enum Granularity  
    {  
        Hourly,  
        Daily,  
        Weekly,  
        Monthly,  
        Quarterly,  
        Yearly  
    }  

    private string GenerateHashedString(string stringToBeHashed)  
    {  
        var algorithm = HashAlgorithmProvider.OpenAlgorithm(HashAlgorithmNames.Sha1);  
        var buffer = CryptographicBuffer.ConvertStringToBinary(stringToBeHashed, BinaryStringEncoding.Utf8);  
        var hashedData = algorithm.HashData(buffer);  
        return CryptographicBuffer.EncodeToHexString(hashedData);  
    }  

    // If this was a .Net app you could use the following to create the hash  

    //public string GenerateHashedString(string plainString)  
    //{  
    //    var encodedString = Encoding.UTF8.GetBytes(plainString);  
    //    var computedHash = SHA1.Create().ComputeHash(encodedString);  
    //    const string hexFormatting = "x2";  

    //    var hashedResult = new StringBuilder();  

    //    foreach (var computedByte in computedHash)  
    //    {  
    //        hashedResult.Append(computedByte.ToString(hexFormatting));  
    //    }  
    //    return hashedResult.ToString();  
    //}  

[/sourcecode]

And equally messy demo UI:

[sourcecode language=“xml”]

<Grid Margin="10,0">  

    <Grid.RowDefinitions>  
        <RowDefinition Height="auto" />  
        <RowDefinition Height="auto" />  
        <RowDefinition Height="auto" />  
        <RowDefinition Height="auto" />  
        <RowDefinition />  
    </Grid.RowDefinitions>  
    <Grid.ColumnDefinitions>  
        <ColumnDefinition />  
        <ColumnDefinition />  
    </Grid.ColumnDefinitions>  
    <TextBox PlaceholderText="Username"  
             x:Name="username" />  
    <TextBox Grid.Column="1"  
             PlaceholderText="Project ID"  
             x:Name="projectId" />  
    <PasswordBox Grid.Row="1"  
                 Grid.ColumnSpan="2"  
                 PlaceholderText="Password"  
                 x:Name="passwordBox" />  
    <Grid Grid.ColumnSpan="2"  
          Grid.Row="2">  
        <Grid.ColumnDefinitions>  
            <ColumnDefinition />  
            <ColumnDefinition Width="auto" />  
            <ColumnDefinition />  
        </Grid.ColumnDefinitions>  
        <DatePicker Background="LightSeaGreen"  
                    x:Name="startDatePicker"  
                    Grid.Column="0" />  
        <TextBlock Grid.Column="1"  
                   FontFamily="Segoe UI Symbol"  
                   FontSize="25"  
                   VerticalAlignment="Center"  
                   Foreground="LightSeaGreen"  
                   Text="&#xE0AE;"  
                   Margin="10,0"  
                   HorizontalAlignment="Center" />  
        <DatePicker Background="LightSeaGreen"  
                    x:Name="endDatePicker"  
                    Grid.Column="2" />  
    </Grid>  

    <Button Grid.Row="3"  
            Grid.ColumnSpan="2"  
            Background="Transparent"  
            HorizontalAlignment="Center"  
            Content="&#xE174;"  
            Click="Button\_Click"  
            FontFamily="Segoe UI Symbol"  
            FontSize="35"  
            Margin="-10"  
            Foreground="Yellow"  
            BorderThickness="0" />  
    <ScrollViewer Grid.Row="4">  
        <TextBlock FontSize="15"  
                   x:Name="jsonResult" />  
    </ScrollViewer>  
    <ScrollViewer Grid.Column="1"  
                  FontSize="15"  
                  Grid.Row="4">  
        <TextBlock x:Name="result" />  
    </ScrollViewer>  
    <Border x:Name="errorBox"  
            Background="red"  
            Grid.Row="4"  
            VerticalAlignment="Top"  
            Grid.ColumnSpan="2"  
            Visibility="Collapsed">  
        <TextBlock x:Name="errorMessage"  
                   Text="Error"  
                   TextWrapping="WrapWholeWords"  
                   Margin="5,10"  
                   TextAlignment="Center"  
                   FontSize="20" />  
    </Border>  

</Grid>  

Happy coding!

Comments

Leave a comment below, or by email.
J. Carlos
11/11/2014 4:44:38 AM
Pretty cool!

I used to work with  SHA1CryptoServiceProvider but I didn't know the way to do so in Win RT. 


Last modified on 2014-11-10

comments powered by Disqus