Obteniendo datos de Google Analytics con CSharp
mayo 15th, 2009La 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:

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?
- En Visual Studio agregamos la clase a nuestra librerÃa.
- Analizamos qué datos queremos obtener en el DataTable.
- Creamos la consulta basandonos en las métricas, dimensiones, filtrados, etc…
- Llamamos al método GetData() enviándole como parámetros la consulta.
- 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








Hola!
A veces soy un rompe huevos, pero… En inglés no lleva signo de apertura en la interrogación: “¿Is this useful?”.
Saludos.
Jejeje.. muchas gracias por el comentario! No me habÃa dado cuenta
Voy a ver si me pongo un poco más con el blog ya que lo tengo bastante descuidado.
Gracias nuevamente,
Un abrazo,