Fliuva, o como crear un servicio de analíticas

Ya hemos visto los motivos que tengo para diseñar un servicio de analíticas desde 0. Ahora veremos como lo he hecho.
Fliuva

No he tocado ningún CSS.

La base de datos

En un servicio de analíticas lo más importante son los datos, así que debemos de definir como almacenaremos la información. En principio Fliuva iba a estar centrado en analizar una única aplicación. Más tarde he pensado en un soporte multiaplicaciones pero no lo voy a desarrollar de momento. Usaría un espacio de nombres delante de todas las tablas y las peticiones GET.

Usuarios = Sesiones

En ciertos servicios de analíticas los usuarios y las sesiones son dos conceptos diferentes. En Fliuva sin embargo, y por razones de simplificar la estructura, ambos conceptos son idénticos y hablaremos de ellos como sesiones.

Tablas

Será necesario tener una tabla de eventos.

Events

  • CATEGORY: Categoría del evento
  • SUBCATEGORY: Subcategoría del evento
  • NAME: Nombre del evento
  • DESCRIPTION: Descripción del evento
  • DATA: Datos del evento
  • ID: Identificador
  • TIME: Hora en que se produjo
  • SESSION: Sesión a la que pertenece el evento

CREATE TABLE IF NOT EXISTS EVENTS(ID INT NOT NULL AUTO_INCREMENT, CATEGORY TEXT, SUBCATEGORY TEXT, NAME TEXT, DESCRIPTION TEXT, DATA TEXT, TIME DATETIME, SESSION TEXT, PRIMARY KEY (`ID`) )

Y ya con esta tabla tendremos para almacenar muchos datos. Los campos CATEGORY, SUBCATEGORY, NAME, DESCRIPTION y DATA sirven unicamente para organizar eventos y subeventos en categorías. Los nombres de los campos son triviales. DESCRIPTION no guarda realmente la descripción sino que podemos definir otro subevento. Con 5 campos para categorizar eventos superamos a Google Analytics que tiene CATEGORY, ACTION, LABEL y VALUE. Además VALUE debe ser numérico mientras que en Fliuva todos son de tipo texto (en la práctica, cualquier cosa).

Código de seguimiento

¿Cómo introducir datos en la base de datos desde nuestra aplicación? Con una pequeña llamada GET a /collect. Yo la he definido en un fichero llamado collect.js


var mysql=require("mysql");

// GET /collect

module.exports=function(req,res){
var connection=mysql.createConnection({
    host: process.env.OPENSHIFT_MYSQL_DB_HOST,
    port: process.env.OPENSHIFT_MYSQL_DB_PORT,
    user: process.env.OPENSHIFT_MYSQL_DB_USER,
    password: process.env.OPENSHIFT_MYSQL_DB_PASSWORD,
    database: "fliuva"
});
connection.connect(function(err){
    if(err){
        res.send(501,"MySQL connection error\n"+err);
    }
    connection.query("CREATE TABLE IF NOT EXISTS EVENTS(ID INT NOT NULL AUTO_INCREMENT,"+
        "CATEGORY TEXT, SUBCATEGORY TEXT, NAME TEXT, DESCRIPTION TEXT, DATA TEXT, "+
        "TIME DATETIME, SESSION TEXT,"+
        "PRIMARY KEY (`ID`) )",function(err,results,fields){
            if(err){
                res.send(501,"MySQL table creation error\n"+err);
            }
            connection.query("INSERT INTO EVENTS SET ?",{
                SESSION: req.query.SESSION,
                TIME: new Date(),
                CATEGORY: req.query.CATEGORY || "",
                SUBCATEGORY: req.query.SUBCATEGORY || "",
                NAME: req.query.NAME || "",
                DESCRIPTION: req.query.DESCRIPTION || "",
                DATA: req.query.DATA || ""
            },function(err,results,fields){
                if(err){
                    res.send(501,"Query error\n"+err);
                }
                res.send("OK");
            });
       });
});
}

Y se llama muy fácilmente


GET /collect?CATEGORY=<category>&SUBCATEGORY=<subcategory>&NAME=<name>&DESCRIPTION=<description>&DATA=<data>&SESSION=<session>

Si estamos en HTML5, necesitaremos una librería de cliente para poder realizar las llamadas


function login(){
    var xhr=new XMLHttpRequest();
    xhr.open("GET","/uuid");
    xhr.addEventListener("load",function(){
        sessionStorage.__fliuvaSession=xhr.responseText;
    });
    xhr.send();
}

function sendEvent(category,subcategory,name,description,data){
    var xhr=new XMLHttpRequest();
    var url="/collect"+
    "?CATEGORY="+category+
    "&SUBCATEGORY="+subcategory+
    "&NAME"+name+
    "&DESCRIPTION"+description+
    "&DATA"+data+
    "&SESSION"+sessionStorage.__fliuvaSession;
    xhr.open("GET",url);
    xhr.send();
}

Así, simplemente hay que llamar a sendEvent. Llamar a login no es necesario siempre que rellenes el valor de sessionStorage.__fliuvaSession correctamente.

Análisis de datos

Ahora debemos analizar los datos. Primero debemos obtener los valores a analizar. La llamada a /get devuelve un fichero JSON con la información completa. En un futuro lo ideal sería espeficicar intervalos de fechas.


//GET /get
var mysql=require("mysql");

module.exports=function(req,res){
var connection=mysql.createConnection({
    host: process.env.OPENSHIFT_MYSQL_DB_HOST,
    port: process.env.OPENSHIFT_MYSQL_DB_PORT,
    user: process.env.OPENSHIFT_MYSQL_DB_USER,
    password: process.env.OPENSHIFT_MYSQL_DB_PASSWORD,
    database: "fliuva"
});
connection.query("SELECT * FROM EVENTS",function(err,results,fields){
    res.send(JSON.stringify(results));
});
}

Y en el cliente tratamos los datos. Aquí es donde debemos nosotros mismos crear las estadísticas según las métricas que hayamos definido. Yo solo voy a poner una métrica universal, usuarios por día.


/* Visualization App */
window.addEventListener("load",function(){
    usersPerDay();
    sessionsTable();
});

function id(idx){
    return document.getElementById(idx);
}

function uniqBy(a, key) {
    var seen = {};
    return a.filter(function(item) {
        var k = key(item);
        return seen.hasOwnProperty(k) ? false : (seen[k] = true);
    })
}
function ISODateString(d){
    function pad(n){return n<10 ? '0'+n : n}
    return d.getUTCFullYear()+'-'
    + pad(d.getUTCMonth()+1)+'-'
    + pad(d.getUTCDate());
}

/* Users-per-day */
function usersPerDay(){
    var xhr=new XMLHttpRequest();
    xhr.overrideMimeType("application/json");
    xhr.open("GET","/get");
    xhr.addEventListener("load",function(){
        var json=JSON.parse(xhr.responseText);
        var dataset=new vis.DataSet();
        /*var data=json.filter(function(){

        });*/
        var array=uniqBy(json,function(item){
            return item.SESSION;
        }); // Eventos de sesiones repetidas eliminados (mismos usuarios). Ahora tenemos sesiones únicas y tiempos distintos
        for(var i=0;i<array.length;i++)
        {
            var time=new Date(array[i].TIME);
            var date=ISODateString(time); //time.toISOString().substring(0,time.toISOString().indexOf("T"));

            var y;
            if(dataset.get(date)==null)
                y=1;
            else
                y=dataset.get(date).y+1;

            console.log(date);
            dataset.update({x: date, id: date, y: y});
        }
        var options = {
            catmullRom: false
        };
        var graph2d = new vis.Graph2d(id("users-per-day"), dataset, options);
    });
    xhr.send();
}

/* Table for sessions */

function sessionsTable(){
	var table=document.getElementById("sessions");
	var xhr=new XMLHttpRequest();
	xhr.open("GET","/get");
	xhr.addEventListener("load",function(){
		var json=JSON.parse(xhr.responseText);
		var data=uniqBy(json,function(item){
			return item.SESSION;
		});
		for(var i=0;i<data.length;i++)
		{
			var item=data[i];
			var tr=document.createElement("tr");
			var time=document.createElement("td");
			time.textContent=item.TIME;
			var session=document.createElement("td");
			var link=document.createElement("a");
			link.href="/session/"+item.SESSION;
			link.textContent=item.SESSION;
			session.appendChild(link);
			tr.appendChild(time);
			tr.appendChild(session);
			table.appendChild(tr);
		}
	});
	xhr.send();
}

Que se corresponde a este pequeño HTML


<!DOCTYPE html>
<html>
	<head>
		<title>Fliuva - Página principal</title>
		<meta charset="utf-8"/>
		<script src="bower_components/vis/dist/vis.min.js" type="text/javascript"></script>
		<link href="bower_components/vis/dist/vis.min.css" rel="stylesheet" media="all" type="text/css">
		<script src="/app.js"></script>
	</head>
	<body>
		<h1>Fliuva</h1>
		<section>
			<h3>Usuarios por día</h3>
			<div id="users-per-day"></div>
		</section>
		<section>
			<table id="sessions">
				<tr>
					<th>Tiempo</th>
					<th>Sesión</th>
				</tr>
			</table>
		</section>
	</body>
</html>

Visualizar cada sesión por separado es posible con la llamada a /session/NOMBRE_DE_SESION


var mysql=require("mysql");

module.exports=function(req,res){
	var session=req.params.session;
	var connection=mysql.createConnection({
		host: process.env.OPENSHIFT_MYSQL_DB_HOST,
		port: process.env.OPENSHIFT_MYSQL_DB_PORT,
		user: process.env.OPENSHIFT_MYSQL_DB_USER,
		password: process.env.OPENSHIFT_MYSQL_DB_PASSWORD,
		database: "fliuva"
	});
	connection.query("SELECT * FROM EVENTS WHERE SESSION = ?",[session],function(err,results){
		if(err)
			res.send(502,"Error: "+err);
		res.render("session.jade",{events: results});
	});
}

Y para un rápido procesamiento he decidido usar Jade con JavaScript en el servidor. Y entonces session.jade queda


doctype html
html
	head
		title Vista de sesión
		meta(charset="utf-8")
	body
		h1 Vista de sesión
		table
			tr
				th Categoría
				th Subcategoría
				th Nombre
				th Descripción
				th Datos
				th Tiempo
			each event in events
				tr
					td= event.CATEGORY
					td= event.SUBCATEGORY
					td= event.NAME
					td= event.DESCRIPTION
					td= event.DATA
					td= event.TIME

Juntando piezas

Por último, la aplicación se tiene que iniciar en algún lado. Server.js contiene el arranque


var express=require("express");
var mysql=require("mysql");
var collect=require("./collect");
var getdata=require("./getdata");
var session=require("./session");
var uuid=require("node-uuid");

var app=express();

app.set("views",__dirname + "/jade");
app.set("view engine","jade");

app.get("/collect",collect);

app.get("/get",getdata);

app.get("/uuid",function(req,res){
	res.send(uuid.v4());
});

app.get("/session/:session",session);

app.use(express.static("www"));

var ip=process.env.OPENSHIFT_NODEJS_IP || process.env.OPENSHIFT_INTERNAL_IP || "127.0.0.1";
var port=process.env.OPENSHIFT_NODEJS_PORT || process.env.OPENSHIFT_INTERNAL_PORT || 8080;

var server=app.listen(port,ip);

Y así en un pis pas hemos hecho una aplicación de seguimiento y analíticas en JavaScript. Ahora toca empezar a diseñar estadísticas con los datos que tenemos a nuestra disposición y por supuesto cuando tengamos los datos a obrar en consecuencia.

La información es poder

La información es poder. La frase suele atribuirse a Francis Bacon y creo que en estos tiempos cada vez se vuelve más cierta. Términos como Big Data, políticas de privacidad y similares son términos para hablar del gran poder que nos ofrece la información, si la sabemos interpretar y usar en consecuencia.

Obteniendo la información

Esto venía para explicar que finalmente y después de pensarlo un rato he decidido crear mi propio sistema de analíticas para Secta Sectarium. Las analíticas pueden ofrecerme valiosa información pero no encontré ningún servicio que me gustase. Muchos sistemas de analíticas están centrados en blogs (como Google Analytics o New Relic Browser) o en aplicaciones móviles (Google Analytics, Flurry). ¿Había algún servicio dedicado solo a juegos? Sí, GameAnalytics es específico pero no tiene API para HTML5 (y las APIs REST no se pueden llamar entre dominios en HTML5, CORS se llama la idea). Google Analytics se puede modificar lo suficiente para funcionar pero ya que tenía que trabajarmelo he preferido crear mi propia solución.

LaGenteSeInventaEstadisticas

Fliuva

Así que he decidido gastar una gear de OpenShift para una aplicación Node.js 0.12 y MySQL 5.5. Entre SQL y NoSQL he elegido SQL porque para introducir datos de eventos que luego, posteriormente, van a ser tratados, SQL da un mejor rendimiento y el esquema de tabla es más común. Las analíticas las podré ver desde la propia aplicación, que usa Vis.js para la visualización.

LaPersonaMasPoderosa

En próximas entradas veremos como se puede crear Fliuva y que métricas son más importantes.

Crea tu primer paquete para Haiku

Hoy vamos a crear nuestro propio paquete para Haiku. Se trata de mi juego SuperFreeCell que diseñé para el Google Code-In 2014.

SuperFreeCell-1

Nuestra propia rama de desarrollo

Si queremos publicar nuestros cambios a Haikuports, debemos hacernos una cuenta en BitBucket y hacer un fork del repositorio. Esto se hace desde http://github.com/haikuports/haikuports y dándole al botón de fork o dividir. En mi caso creé el repositorio https://bitbucket.org/AdrianArroyoCalle/haikuports-superfreecell. Añadimos el repositorio a nuestra lista de orígenes en Git.


git remote add superfreecell https://bitbucket.org/AdrianArroyoCalle/haikuports-superfreecell

Ubicando el juego en Haikuports

Nada más empezar tenemos que encontrar la categoría a la que pertenecerá el paquete. Cada carpeta dentro de la carpeta haikuports representa una categoría, que siguen el esquema de Gentoo. En mi caso creo que lo conveniente es “haiku-games”. Dentro de esta carpeta creamos una con el nombre de nuestro paquete y allí almacenaremos la información sobre nuestro paquete. Estas carpetas deben tener al menos un archivo .recipe y pueden incluir las carpetas licenses y patches con licencias y parches adicionales respectivamente. En mi caso no usaremos ninguna de estas dos carpetas, así que creamos el archivo superfreecell-0.1.0.recipe . Es importante esta estructura para encontrar la versión fácilmente.

El archivo .recipe

El archivo .recipe contiene la información necesaria de metadatos e instrucciones para compilar e instalar el programa en cuestión.


SUMMARY=&quot;Descripción de menos de 70 caracteres&quot;
DESCRIPTION=&quot;
Descripción extensa del programa usando \
para separar entre renglones que no deben superar \
los 80 caracteres
&quot;
HOMEPAGE=&quot;http://pagina-de-inicio.org.es&quot;
SOURCE_URI=&quot;http://una-pagina.com/con-el-archivo-del-codigo-fuente.tar.gz&quot; # Se admiten muchas variaciones aquí. Podemos usar git://, hg://, svn://, bzr://, ftp://, http:// y los formatos de compresión tar.gz, tar.bz2, zip. Se admiten combinaciones de protocolos.
LICENSE=&quot;MIT&quot;
COPYRIGHT=&quot;Año y autor&quot;
REVISION=&quot;1&quot; # Siendo la misma versión del programa, revisiones del propio empaquetado
ARCHITECTURES=&quot;?x86 x86_gcc2 !x86_64&quot; # Arquitecturas compatibles, siendo x86_gcc2 estable, x86 sin probar (untested) pero que debería ir y x86_64 incompatible
if [ $effectiveTargetArchitecture != x86_gcc2 ]; then
ARCHITECTURES=&quot;$ARCHITECTURES x86_gcc2&quot;
fi
SECONDARY_ARCHITECTURES=&quot;x86&quot; # Arquitecturas secundarias
PROVIDES=&quot;
miaplicacion$secondaryArchSuffix = $portVersion # Todos los paquetes se proveen a sí mismos
app:miaplicacion$secondaryArchSuffix = $portVersion # además es una aplicación de Haiku accesible desde los menús
&quot;

REQUIRES=&quot;
haiku$secondaryArchSuffix # Bastante claro. Aquí vendrían librerías en tiempo de ejecución
&quot;

BUILD_REQUIRES=&quot;
haiku_devel$secondaryArchSuffix # Actualmente todos los Haiku tienen haiku_devel pero por si las moscas. Aquí vendrían librerías de desarrollo
&quot;

BUILD_PREREQUIRES=&quot;
cmd:gcc$secondaryArchSuffix # Aquí vendrían las herramientas de línea de comandos. El prefijo cmd: indica que se llama desde la línea de comandos
cmd:ld$secondaryArchSuffix
cmd:make # Hay herramientas que da igual en que arquitectura estén para funcionar correctamente
&quot;

SOURCE_DIR=&quot;LA_CARPETA_CON_EL_CODIGO_FUENTE&quot;

PATCH()
{
# Aquí se ponen los parches que se puedan aplicar son sed
}
BUILD()
{
# Las instrucciones de configuración y compilación. Si usamos autotools
runConfigure ./configure
make
# Si usamos CMake
cmake .
make

}
INSTALL()
{
# Los comandos para instalar la aplicación. Hay que tener cuidado con los directorios especiales de Haiku, que no son POSIX
# Si usamos autotools y CMake
make install

# También podemos copiar manualmente
mkdir -p $includeDir
cp include/libreria.h $includeDir/
# Hay unas cuantas variables de carpetas que podemos usar

addAppDeskbarSymlink $appsDir/MiApplicacion
# Es muy posible que el make install no instale los enlaces para mostrarse en el lanzador de aplicaciones de Haiku
}

Y este sería el caso más básico de receta que podemos hacer. Si empaquetamos librerías la cosa se complica un poco más ya que tenemos que distinguir la parte de ejecución de la parte de desarrollo y entonces tendremos secciones como PROVIDES_devel que es especifico al paquete de desarrollo. Hay otra manera de aplicar parches que es con patchsets. Nosotros editamos la aplicación hasta que funcione y Haikuporter nos generará un archivo con los cambios que hay que aplicar a las fuentes. Es el mejor método para software un poco más complejo.

SuperFreeCell-Recipe

Publicar cambios

Una vez hayamos comprobado que funciona, lo subimos a nuestra copia de Git.


git add haiku-games/superfreecell
git commit -m &quot;SuperFreeCell 0.1.0&quot;
git push superfreecell master

Y desde BitBucket hacemos una pull request o solicitud de integración

Instala programas en Haiku con HaikuPorts y HaikuPorter

Haiku introdujo recientemente su nuevo sistema de paquetería. Este dispone de varios métodos para obtener los programas. El método más sencillo es HaikuDepot, la aplicación gráfica con paquetes ya compilados listos para descargar e instalar con un click. Sin embargo hay mucho más software disponible en el árbol de recetas de Haiku, conocido como HaikuPorts, que usa un programa propio para su gestión llamado HaikuPorter. HaikuPorter no gestiona la instalación, sino la creación de los paquetes desde la fuentes originales.

haiku-depot

Haikuports y Haikuporter

Haikuports es una colección se software en forma de recetas que dicen como se deben de compilar los programas pero no los almacena. Un sistema similar al de Gentoo y FreeBSD. Haikuports usa Haikuporter para construir los paquetes así que debemos instalar antes de nada Haikuports y Haikuporter.

Instalando Haikuporter

Instalar Haikuporter requiere que abras la terminal y obtengamos su código fuente


git clone https://bitbucket.org/haikuports/haikuporter

Ahora debemos configurarlo con nuestros datos


cd haikuporter
cp haikuports-sample.conf /boot/home/config/settings/haikuports.conf
ln -s /boot/home/haikuporter/haikuporter /boot/home/config/non-packaged/bin/
lpe /boot/home/config/settings/haikuports.conf

Tendremos que editar un archivo. Os pongo como tengo el mío


TREE_PATH="/boot/home/haikuports"
PACKAGER="Adrián Arroyo Calle <micorreo@gmail.com>"
ALLOW_UNTESTED="yes"
ALLOW_UNSAFE_SOURCES="yes"
TARGET_ARCHITECTURE="x86_gcc2"
SECONDARY_TARGET_ARCHITECTURES="x86"

Aunque en vuestro caso podeis poner “no” en ALLOW_UNTESTED y ALLOW_UNSAFE_SOURCES.

Instalando Haikuports

Volvemos a nuestra carpeta y obtenemos el código


cd ~
git clone https://bitbucket.org/haikuports/haikuports.git --depth=10

Usando Haikuporter y Haikuports

Ya estamos listo para construir cualquier paquete con Haikuporter, no solo los nuestros. Con esto podemos acceder a gran cantidad de software. El uso básico de haikuporter es


haikuporter NOMBRE_DEL_PAQUETE

Aunque si las dependencias nos abruman podemos saltarnoslo


haikuporter NOMBRE_DEL_PAQUETE --no-dependencies

Los paquetes no se instalan automáticamente, se guardan en /boot/home/haikuports/packages y para instalarlos los debemos copiar a /boot/home/config/packages. También podemos compilar con GCC4 en vez de GCC2 si el programa lo soporta. Hay que añadir _x86 al final. Comprobemos que todo funciona con un paquete al azar


haikuporter cmake_haiku_x86

Tardará en actualizar la base de datos y pueden que nos salten errores de arquitecturas pero no hay que preocuparse. En mi caso, Haikuporter quería instalar paquetes del sistema ya que había versiones nuevas en haikuports. Sin embargo, como se que iba a tardar mucho cancelé y ejecuté


haikuporter cmake_haiku_x86 --no-dependencies

Convendría ahora instalar todo lo compilado


cp /boot/home/haikuports/packages/*.hpkg /boot/home/config/packages/