La poderosa herramienta Google Analytics nos vuelve a sorprender, esta vez con algo que muchos esperabamos: una API para exportar datos.

Esta novedosa API nos permite incluir datos obtenidos por Google Analytics en nuestras aplicaciones.

¿De qué sirve esto?

De mucho. Nos podemos imáginar infinidades de usos; desde la simple colocación de datos en nuestro CMS hasta la creación de una nube de tags.

Mi experiencia personal fue la colocación en el CMS de la empresa en la que trabajo, con un muy pintoresco resultado:

Custo Graphics With Google Analytics

El desafío

Ya que nuestro CMS está basado en .NET, y dado el poco soporte que Google brinda a esta plataforma, implementé mi propia clase para obtener datos.
Esta clase se encarga de convertir las feeds XML que envía google en una estructura mucho más práctica y fácil de usar en .NET: el DataTable.

Es de fácil uso y está comentada para que se pueda entender con facilidad. De todas maneras es conveniente que estudien un poco el prototipo que expone Google para la exportación de datos: http://code.google.com/apis/analytics/docs/gdata/1.0/gdataProtocol.html

Para realizar las consultas deben de tener en cuenta las dimensiones y métricas que son las que nos permiten seleccionar qué datos queremos traer; por ejemplo le podemos decir que queremos la dimensión ga:pagePath y la métrica ga:pageviews lo que nos daría como resultado, la cantidad de visitas que tuvo cada una de las páginas de nuestro sitio. Otro ejemplo más sencillo sería obtener las visitas totales que tuvo nuestro sitio por fecha, esto lo podemos lograr utilizando la dimensión ga:date y la métrica ga:visits

¿Cómo comienzo a utilizarlo?

  1. En Visual Studio agregamos la clase a nuestra librería.
  2. Analizamos qué datos queremos obtener en el DataTable.
  3. Creamos la consulta basandonos en las métricas, dimensiones, filtrados, etc…
  4. Llamamos al método GetData() enviándole como parámetros la consulta.
  5. Obtenemos un DataTable que podemos utilizar como queramos.

Ejemplos de llamados al método:

  • Para obtener las visitas al sítio por día del último mes ordenadas por fecha asc:
    DataTable data = GetData(new string[] { “ga:date” }, new string[] { “ga:visits” }, new string[] { “ga:date” }, new string[] { }, DateTime.Today.AddMonths(-1), DateTime.Today, 0, 0);
  • Para obtener la fuente de las visitas (de donde llegan a nuestro sitio) del último mes ordenado por fuente:
    DataTable data = GetData(new string[] { “ga:source” }, new string[] { “ga:visits” }, new string[] { “ga:source” }, new string[] { }, DateTime.Today.AddMonths(-1), DateTime.Today, 0, 0);
  • Para obtener la cantidad de visitas que tuvo cada página de nuestro sitio en el último mes ordenado por cantidad de visitas desc:
    DataTable data = GetData(new string[] { “ga:pagePath” }, new string[] { “ga:pageviews” }, new string[] { “-ga:pageviews” }, new string[] { }, DateTime.Today.AddMonths(-1), DateTime.Today, 0, 0);
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Text;
using System.Net;
using System.Xml;
using System.Data;

public class GAnalytics
{
    #region Atributos

    private string _username = null;
    private string _password = null;
    private string _profileId = null;
    private string _token = null;
    private static readonly string authUrlFormat = "accountType=GOOGLE&Email={0}&Passwd={1}&source=reimers.dk-analyticsreader-0.1&service=analytics";

    #endregion

    public GAnalytics() { }

    public GAnalytics(string username, string password)
    {
        _username = username;
        _password = password;
    }

    public GAnalytics(string username, string password, string profileId)
    {
        _username  = username;
        _password  = password;
        _profileId = profileId;
    }

    #region Properties

    public string Username
    {
        get
        {
            return _username;
        }
        set
        {
            _username = value;
        }
    }

    public string Password
    {
        get
        {
            return _password;
        }
        set
        {
            _password = value;
        }
    }

    public string Token
    {
        get
        {
            if (string.IsNullOrEmpty(_token))
            {
                _token = GetToken();
            }
            return _token;
        }
    }

    public string ProfileId
    {
        get
        {
            return _profileId;
        }
        set
        {
            _profileId = value;
        }
    }

    #endregion

    #region Metodos

    ///
    /// Loguea al usuario en Google y obtiene el Token
    ///
    ///
    private string GetToken()
    {
        try
        {
            if (string.IsNullOrEmpty(Username) || string.IsNullOrEmpty(Password))
            {
                throw new ArgumentNullException("Username, Password", "Username and/or password not set");
            }

            string authBody = string.Format(authUrlFormat, Username, Password);
            HttpWebRequest req = (HttpWebRequest)HttpWebRequest.Create("https://www.google.com/accounts/ClientLogin");
            req.Method = "POST";
            req.ContentType = "application/x-www-form-urlencoded";
            req.UserAgent = "CSharp Application";
            Stream stream = req.GetRequestStream();
            StreamWriter sw = new StreamWriter(stream);
            sw.Write(authBody);
            sw.Close();
            sw.Dispose();
            HttpWebResponse response = (HttpWebResponse)req.GetResponse();
            StreamReader sr = new StreamReader(response.GetResponseStream());
            string token = sr.ReadToEnd();
            string[] tokens = token.Split(new string[] { "\n" }, StringSplitOptions.RemoveEmptyEntries);
            foreach (string item in tokens)
            {
                if (item.StartsWith("Auth="))
                {
                    return item.Replace("Auth=", "");
                }
            }
        }
        catch (Exception e)
        {
            throw (e);
        }
        return string.Empty;
    }

    ///
    /// Parsea la fecha en formato DateTime
    ///
    ///
Fecha en formato yyyy-mm-dd
    ///
    private DateTime ParseDate(string date)
    {
        return new DateTime(int.Parse(date.Substring(0, 4)), int.Parse(date.Substring(4, 2)), int.Parse(date.Substring(6, 2)));
    }

    ///
    /// Parsea un documento xml en una DataTable.
    ///
    ///
Documento XML retornado por el servidor
    ///
Dimensiones de la petición
    ///
Métricas de la petición
    ///
    private DataTable ParseXml(XmlDocument xml, string[] dimensions, string[] metrics)
    {
        DataTable data = new DataTable();
        foreach (string dimension in dimensions)
        {
            if (dimension == "ga:date")
            {
                data.Columns.Add(dimension, typeof(DateTime));
            }
            else
            {
                data.Columns.Add(dimension);
            }
        }
        foreach (string metric in metrics)
        {
            data.Columns.Add(metric);
        }

        foreach (XmlNode node in xml.ChildNodes[1])
        {
            if (node.Name.Equals("entry", StringComparison.CurrentCulture))
            {
                DataRow row = data.NewRow();
                foreach (XmlNode nodeValue in node.ChildNodes)
                {
                    if (nodeValue.Attributes["name"] != null)
                    {
                        foreach (string dimension in dimensions)
                        {
                            if (nodeValue.Attributes["name"].Value == dimension)
                            {
                                if (dimension == "ga:date")
                                {
                                    row[dimension] = ParseDate(nodeValue.Attributes["value"].Value);
                                }
                                else
                                {
                                    row[dimension] = nodeValue.Attributes["value"].Value;
                                }
                            }
                        }
                        foreach (string metric in metrics)
                        {
                            if (nodeValue.Attributes["name"].Value == metric)
                            {
                                row[metric] = nodeValue.Attributes["value"].Value;
                            }
                        }
                    }
                }
                data.Rows.Add(row);
            }
        }
        return data;
    }

    ///
    /// Perfiles a los cuales tiene acceso el usuario
    ///
    ///
    public NameValueCollection GetProfiles()
    {
        //Obtengo la respuesta
        string response = GetResponse("https://www.google.com/analytics/feeds/accounts/default");

        //Obtengo el nodo correspondiente con los perfiles sobre los cuales el usuario tienen permisos
        XmlDocument accountinfoXML = new XmlDocument();
        accountinfoXML.LoadXml(response);
        XmlNodeList entries = accountinfoXML.GetElementsByTagName("entry");

        //Cargo los perfiles
        NameValueCollection profiles = new NameValueCollection();
        for (int i = 0; i < entries.Count; i++)
        {
            profiles.Add(entries.Item(i).ChildNodes[2].InnerText, entries.Item(i).ChildNodes[7].Attributes["value"].Value);
        }

        return profiles;
    }

    //Atajo sin ProfileId
    public DataTable GetData(string[] dimensions, string[] metrics, string[] sort, string[] filters, DateTime startDate, DateTime endDate, int resultLimit, int pageIndex)
    {
        return GetData(dimensions, metrics, sort, filters, startDate, endDate, resultLimit, pageIndex, ProfileId);
    }

    ///
    /// Retorna un DataTable con los resultados para los parametros indicados.
    ///
    ///
Optional. Specifies the dimensions included in the report and by which metrics will be segmented. A single request is limited to a maximum of 7 dimensions.
    /// dimensions=ga:country,ga:browser. For more information, see http://code.google.com/apis/analytics/docs/gdata/1.0/gdataProtocol.html#requestingDimensions
    ///
Optional.Specifies the metrics included in the report. A single request is limited to a maximum of 10 metrics.
    /// metrics=ga:pageviews,ga:uniquePageviews. For more information, see http://code.google.com/apis/analytics/docs/gdata/1.0/gdataProtocol.html#requestingMetrics
    ///
Optional. Specifies sort order and direction for the entries in the feed.
    /// sort=ga:browser,ga:pageviews. For more information, see http://code.google.com/apis/analytics/docs/gdata/1.0/gdataProtocol.html#sorting
    ///
Optional. Specifies filters to apply to the data in your profile to return a sub-set of entries. filters=ga:country%3D%3DCanada
    /// For more information, see http://code.google.com/apis/analytics/docs/gdata/1.0/gdataProtocol.html#filtering
    ///
Required. All Analytics feed requests must specify a beginning and ending date range. If you do not indicate start- and end-date values for the request, the server will return a 400 Bad request error. Date values are in the form YYYY-MM-DD. See Common Query Parameters for more details. start-date=2008-07-22 end-date=2008-08-22
    ///
Required. All Analytics feed requests must specify a beginning and ending date range. If you do not indicate start- and end-date values for the request, the server will return a 400 Bad request error. Date values are in the form YYYY-MM-DD. See Common Query Parameters for more details. start-date=2008-07-22 end-date=2008-08-22
    ///
To manipulate the result set for a large number of entries, you can use max-results parameter. Value between 0 and 1000. Use it in 0 to get all enteries
    ///
Número de página de la cual se quiere mostrar el resultado. 0 para todas
    ///

    /// Datatable con todos los resultados
    public DataTable GetData(string[] dimensions, string[] metrics, string[] sort, string[] filters, DateTime startDate, DateTime endDate, int resultLimit, int pageIndex, string profileId)
    {
        if (string.IsNullOrEmpty(profileId))
        {
            throw new ArgumentNullException("ProfileId cannot be empty");
        }
        if (dimensions.Length == 0 || metrics.Length == 0)
        {
            throw new ArgumentException("There must be at least one dimension and one metric");
        }

        string url = "https://www.google.com/analytics/feeds/data?ids=ga:" + profileId;

        //Agrego las dimensiones
        if (dimensions.Length > 0)
        {
            url += "&dimensions=" + dimensions[0];
            for (int i = 1; i < dimensions.Length; i++)
            {
                url += "," + dimensions[i];
            }
        }

        //Agrego las métricas
        if (metrics.Length > 0)
        {
            url += "&metrics=" + metrics[0];
            for (int i = 1; i < metrics.Length; i++)
            {
                url += "," + metrics[i];
            }
        }

        //Agrego los parámetros de ordenamiento
        if (sort.Length > 0)
        {
            url += "&sort=" + sort[0];
            for (int i = 1; i < sort.Length; i++)
            {
                url += "," + sort[i];
            }
        }

        //Agrego los parámetros de filtrado
        if (filters.Length > 0)
        {
            url += "&filters=" + filters[0];
            for (int i = 1; i < filters.Length; i++)
            {
                url += "," + filters[i];
            }
        }

        //Agrego el rango de fechas
        url += "&start-date=" + startDate.ToString("yyyy-MM-dd");
        url += "&end-date=" + endDate.ToString("yyyy-MM-dd");

        //Agrego la cantidad máxima de filas. Si es 0 el se traerán todos los registros
        if (resultLimit > 0)
        {
            url += "&max-results=" + resultLimit;
        }
        else
        {
            url += "&max-results=1000"; //1000 es el valor máximo de carga permitido por google
        }

        //Si tengo más de una página agrego el número de página
        if (pageIndex > 0)
        {
            url += "&start-index=" + pageIndex;
        }
        else
        {
            url += "&start-index=1";
        }

        //Obtengo el xml de respuesta
        XmlDocument xml = new XmlDocument();
        xml.LoadXml(GetResponse(url));

        int results = GetResultNumber(xml);
        double totalPages = results / 1000;
        totalPages = Math.Ceiling(totalPages);

        //Si quiero todos los resultados y tengo más de una página, llamo recursivamente y mergeo los DataTable
        if (results > 1000 && resultLimit == 0 && pageIndex < totalPages)
        {
            DataTable returnData = ParseXml(xml, dimensions, metrics);
            returnData.Merge(GetData(dimensions, metrics, sort, filters, startDate, endDate, 0, pageIndex + 1, profileId));
            return returnData;
        }
        else
        {
            return ParseXml(xml, dimensions, metrics);
        }
    }

    private int GetResultNumber(XmlDocument xml)
    {
        //Busco el valor del nodo openSearch:totalResults
        foreach (XmlNode node in xml.ChildNodes[1])
        {
            if (node.Name.Equals("openSearch:totalResults", StringComparison.CurrentCulture))
            {
                return int.Parse(node.InnerText);
            }
        }
        return 0;
    }

    ///
    /// Retorna la string de respuesta del servidor
    ///
    ///
URL a la que se debe de realizar la petición
    ///
    private string GetResponse(string url)
    {
        //Creo la petición Http
        HttpWebRequest myRequest = (HttpWebRequest)WebRequest.Create(url);

        //Agrego la cabecera Http para la autenticación
        myRequest.Headers.Add("Authorization: GoogleLogin auth=" + Token);

        try
        {
            //Intento obtener una respuesta
            HttpWebResponse myResponse = (HttpWebResponse)myRequest.GetResponse();
            Stream responseBody = myResponse.GetResponseStream();

            Encoding encode = System.Text.Encoding.GetEncoding("utf-8");
            StreamReader readStream = new StreamReader(responseBody, encode);
            return readStream.ReadToEnd();
        }
        catch (Exception e)
        {
            throw (e);
        }
    }

    #endregion
}

Estaría bueno que compartieran cualquier correción, upgrade del código.

Fuentes:
http://analytics.blogspot.com/2009_05_01_analytics_archive.html
http://www.akamarketing.com/blog/103-introducing-google-analytics-api-with-aspnet-c.html
http://code.google.com/apis/analytics/docs/gdata/1.0/gdataProtocol.html