viernes, 30 de agosto de 2013

weinre: Inspeccionar remotamente dispositivos móviles

Tarde o temprano, cualquier desarrollador web tendrá que hacer una aplicación y testear su funcionamiento en dispositivos móviles. Hasta ahora siempre había confiado en que el navegador de la tableta de turno renderizara correctamente la página. Cierto es que los navegadores de casi cualquier dispositivo móvil han evolucionado enormemente y, utilizando frameworks como jQuery, casi puedes olvidarte de las diferencias entre navegadores desktop y navegadores móviles. Sin embargo (y psicoanalizándome) me percato que tengo una gran dependencia de las herramientas de desarrollo que incluyen los navegadores de escritorio. Es decir, hoy por hoy no sería nadie si me quitas Firebug (Firefox) o las Chrome Developer Tools. Pues bien, cuando estamos “testeando” nuestra aplicación, por ejemplo en un iPad… ¿Dónde están estas herramientas? ¡No hay! <pánico mode=”on” />.

Si tienes la suerte de tener un Mac estás de suerte porque con Safari puedes utilizar las herramientas de desarrollo del navegador de escritorio para depurar el iPad. Yo lo he visto funcionar y va perfecto. Simplemente conectas el iPad a tu Mac y con un par de clics estás depurando la UI del iPad desde el Mac… pero no tengo un Mac :-(

Es en este punto donde quiero explorar distintas vías de depuración remota porque el método actual de ensayo-error, esto es unos cuantos alerts por aquí y unos cuantos console.log por allí, cuando hablamos de CSS puede ser eterno, frustrante diría yo.

Ya he probado Firebug lite, ya sea incluyendo el .js necesario en la página o a través de un bookmarklet. Sin embargo, la experiencia de depuración dista mucho de ser satisfactoria. Sinceramente y en mi humilde opinión, es un poco “quiero y no puedo”.

Hoy mismo y preso de la desesperación, he recordado (no sé cómo) un tweet de @gulnor que hablaba sobre una herramienta de depuración remota multiplataforma. Estoy hablando de weinre. Además, también escribió un post, weinre: El Firebug para móviles que te recomiendo leer porque es la semilla de éste post.

A grandes rasgos, weinre incorpora un inspector web basado en WebKit (básicamente es casi igual a las Chrome Developer Tools) para inspeccionar dispositivos remotos. Es decir, en mi máquina tengo un inspector web que me está mostrando y permitiendo modificar, el contenido que se está mostrando en cualquier dispositivo remoto (que puede ser un iPad, un tableta Android, etc.).

He de reconocer que preparar el entorno para poder utilizar weinre me ha llevado un buen rato de googlear. Es por ello que voy a mostrar los pasos necesarios que he tenido que realizar para poder inspeccionar  “felizmente” mi aplicación web en el iPad.

Antes de instalar y configurar weinre, es necesario configurar IIS Express para que acepte conexiones remotas. Por defecto no lo hace, una pena. En estos posts explican los pasos muy bien Accessing an IIS Express site from a remote computer IIS Express enable external request. Lo único que me volvió un poco loco es el paso 2 y el parámetro user=everyone que en un Windows en español es user=Todos (ya sé que a lo mejor es patético, pero te prometo que me llevó un buen rato caer en la cuenta de la localización y sólo fue gracias a esta respuesta en StackOverflow que lo comentaba http://stackoverflow.com/a/16508154).

Una vez que ya puedo acceder remotamente a mi IIS Express, ahora toca el turno de weinre.

Lo primero es instalar Node.js. Yo he descargado la versión Windows Installer (.msi) de 64 bits desde http://nodejs.org/download. Nada especial, siguiente, siguiente…

Después he instalado el paquete de weinre con el gestor de paquetes npm de Node.js. He abierto una línea de comandos desde el atajo creado en la pantalla de inicio de Windows 8 y he escrito el siguiente comando:

clip_image001

npm -g install weinre

Ahora creo una excepción en el firewall de Windows para el puerto 8080 y arranco weinre con este comando:

weinre --boundHost -all-

En este momento ya podemos acceder a la url http://localhost:8080/client/ y veremos que weinre está ahí, esperando clientes a los que inspeccionar.

clip_image003

Está claro que lo que nos hace falta son clientes.

Para poder inspeccionar remotamente un dispositivo hay que incluir el siguiente script en el código de nuestra aplicación:

<script src="http://nuestra_ip:8080/target/target-script-min.js"></script>

Este script es el que hace mucha de la magia de weinre, comunicándose con el servidor y permitiendo la inspección del cliente remoto a través de llamadas AJAX.

Por último, sólo queda seleccionar un cliente (en terminología weinre, un target) de la lista disponible en http://localhost:8080/client/ e interactuar con los paneles del inspector web de weinre y ver como los cambios que realicemos se reflejarán en el dispositivo móvil inspeccionado.

Aunque weinre promete, lógicamente tampoco espero que el web inspector de weinre llegue tan lejos como llegan las Chrome Developer Tools o Firebug, por ejemplo parece que no se puede depurar Javascript. Sin embargo, cualquier cosa más elaborada que el método ensayo-error, será bienvenido y no me quejaré.

Por cierto, si aún no te quedado del todo claro que es esto de weinre, me he arrancado de forma experimental a grabar un pequeño video con el resultado final del experimento. http://www.youtube.com/watch?v=hT6DymRkNLM

Y por último decirte que si tu dispositivo es Android y utilizas Chrome en el mismo, la mejor solución de depuración remota es utilizar la solución nativa que ofrece google Remote Debugging Chrome on Android.

Un saludo!

viernes, 23 de agosto de 2013

ELMAH en ASP.NET MVC, paso a paso

Registrar los errores de tu aplicación con ELMAH es un buen invento. Puedes guardarlos en una base de datos, enviarlos por correo, filtrar sólo a cierto tipo de excepciones, puedes suscribirte a una fuente RSS y ver los errores en tu lector de noticias preferido, etc. Lo cierto es que para mí es un must-have en casi cualquier proyecto de ASP.NET. Sin embargo, siempre que arranco un proyecto y agrego ELMAH, después me toca configurarlo y recordar el porqué de esos pequeños detalles que hacen que ELMAH funcione como yo quiero.

En este post lo que voy a contar es como yo configuro ELMAH en un proyecto de ASP.NET MVC, cubriendo las necesidades más usuales en cuanto al registro de errores: filtrado, envío por correo, etc.

Como ya sabes, cuando creas un proyecto de ASP.NET MVC, la plantilla incorpora código para agregar un filtro global a todos los controladores a través del atributo HandleErrorAttribute. Esto pasa en la clase FilterConfig que está en el directorio App_Start, y cuyo método RegisterGlobalFilters es llamado desde el evento Application_Start del global.asax.

filters.Add(new HandleErrorAttribute());

Este atributo se encarga de devolver al cliente una vista de error con información relativa al mismo. Este atributo no registra el error en ningún medio persistente. Eso nos gusta porque sólo tiene una responsabilidad y la hace muy bien. No se pega con ELMAH porque hace algo distinto a ELMAH. HandleError gestiona si debe o no mostrar una vista de error al usuario, por el contrario, lo que hará ELMAH será registrar el error en una base de datos y enviar un correo electrónico. Si se muestra o no al usuario una vista de error nada tiene que ver con ELMAH.

Lo que si debe interesarnos de la pareja HandleError y ELMAH, es que ELMAH considera que un error debe ser registrado sino ha sido manejado previamente. ¿Y quién decide si el error ha sido o no manejado? Pues HandleError estableciendo a true o false la propiedad ExceptionHandled.

Para que HandleError marque como manejado el error tienen que cumplirse algunos requisitos como por ejemplo no ser una acción hija (ChildAction), estar activado CustomErrors, etc. La verdad es que con todas las herramientas gratis que hay de decompilación (¿Se dice así?), es fácil ver el código de HandleErrorAttribute y su método OnException. Yo utilizo JustDecompile de Telerik, pero hay otras y además siempre nos quedará explorar directamente el código fuente de ASP.NET MVC, que es open-source… comento… http://aspnetwebstack.codeplex.com/SourceControl/latest#src/System.Web.Mvc/HandleErrorAttribute.cs

En cualquier caso, a propósito de la gestión de errores en ASP.NET MVC, te recomiendo la lectura del post de José María Aguilar Control de errores en acciones ASP.NET MVC donde se explica perfectamente al atributo HandleError mucho más en detalle.

El único aporte que hago al anterior post es que es necesario que la vista de error tenga cierto tamaño en bytes para que IE la muestre. Es raro, lo sé, pero es lo que hay. Míralo aquí http://stackoverflow.com/a/9540840. Yo directamente en mi vista de error agrego siempre al final la siguiente instrucción para curarme en salud:

@(new String(' ', 1000))

Llegados a este punto y sabiendo qué hace y qué no hace HandleErrorAttribute, llega la hora de comenzar a integrar ELMAH en nuestra aplicación.

Lo primero es descargar el paquete de Nuget. Como suelo trabajar con Sql Server, siempre descargo el mismo paquete:

clip_image002

Este paquete puede ser agregado también a proyectos de librerías de clases. En ese caso, simplemente agregará la referencia a Elmah.dll. Yo suele hacerlo para el ensamblado Models porque así puedo utilizar el registro manual de errores también desde este ensamblado, luego veremos cómo.

La configuración manual que hay que realizar a continuación es la siguiente:

  • Completar con información válida la cadena de conexión “elmah-sqlserver” o cambiar el atributo connectionStringName de la sección elmah/errorLog a una cadena de conexión que ya tuviéramos registrada previamente.
  • Ejecutar el código T-SQL que hay en el fichero App_Readme/Elmah.SqlServer.sql. Este código da un warning si no estamos en una base de datos con compatibilidad con Sql Server 2000, pero finalmente se ejecuta con éxito y yo nunca he tenido problemas con ninguna versión superior de Sql Server.

Yo por mi parte y como me gusta recibir al correo los errores de mi aplicación, agrego también la sección elmah/errorMail al web.config raíz.

<errorMail from="tucorreo@tudominio.com" to="tucorreo@tudominio.com" subject="Error" priority="Low" async="true" smtpPort="25" smtpServer="mail.tudominio.com" useSsl="false" userName="tucorreo@tudominio.com" password="tucontraseña" noYsod="false" />


Respecto a la seguridad, por defecto sólo se puede acceder a elmah.axd desde una petición local. Como queremos poder acceder desde fuera a esta pantalla para ver los errores, hay que establecer a true el valor de allowRemoteAccess. https://code.google.com/p/elmah/wiki/SecuringErrorLogPages

<security allowRemoteAccess="true" />

Como ahora hemos abierto al mundo la página elmah.axd, debemos estar seguros de que sólo usuarios con un cierto rol puede acceder a la misma. Esto lo haremos con ayuda de la autorización basada en roles de ASP.NET sólo dejaremos acceder a elmah.axd a los roles Admin.

<authorization>

  <allow roles="admin" />

  <deny users="*" />

</authorization>

Esto hay que meterlo dentro de location path="elmah.axd", elemento que ya nos agregó el paquete de Nuget. De todas formas, no siempre es una buena práctica exponer elmah.axd al exterior, al respecto puedes leer este magnífico post de Luis Ruiz Pavón que ilustra qué fácil es robar la sesión de un usuario y acceder de forma fraudulenta a elmah.axd.

Hasta aquí era la parte fácil de configuración de ELMAH, ahora toca escribir código.

En nuestro ensamblado Models voy a crear una clase de excepción personalizada.

using System;

 

namespace Models

{

    public class ModelException : Exception

    {

        public ModelException()

        {

        }

 

        public ModelException(string message)

            : base(message)

        {

        }

 

        public ModelException(string message, Exception inner)

            : base(message, inner)

        {

        }

    }

}


Esta clase será utilizada después para excluirla a través del filtrado de ELMAH, pero lo dicho, después lo veremos.

En el ensamblado de Models agrego la clase ElmahUtilities que nos servirá para filtrar qué errores se deben o no registrar con ELMAH. Yo me inclino por el filtrado por código, pero también se puede filtrar a través del web.config con la sección elmah/errorFilter. De todas formas, aquí se explica cómo filtrar vía configuración https://code.google.com/p/elmah/wiki/ErrorFiltering

using System;

using System.Web;

 

namespace Models

{

    public static class ElmahUtilities

    {

        public static bool Dismiss(Exception e)

        {

            if (e.GetBaseException() is HttpException)

            {

                var httpCode = ((HttpException)e.GetBaseException()).GetHttpCode();

                if (httpCode == 404)

                {

                    return true;

                }

            }

            if (e.GetBaseException() is ModelException)

            {

                return true;

            }

            return false;

        }

    }

}

Si te estás preguntado cómo es posible que hayamos agregado una referencia a System.Web en el ensamblado Models… Ya sé que no se debe, que el modelo tiene que ser agnóstico de la capa de presentación… pero la realidad es que si lo hago podré después utilizar ELMAH también desde el modelo para registrar errores de forma manual. De todas formas, sino te gusta esto también se podría crear un ensamblado CrossCutting o similar donde poner esta funcionalidad (aunque tendrías que retocar ligeramente el código aquí expuesto, pero vamos… nada que tú no sepas hacer).

Volviendo a la clase ElmahUtilities, lo que hace el método Dismiss es devolver true, lo que descartará y no registrará la excepción, cuando se cumpla alguna de las siguientes condiciones:

  • Es una excepción de tipo Http y el error es 404.
  • Es una excepción del tipo ModelException (la que creamos personalizada en un paso anterior).

Lógicamente, eres libre de implementar la lógica de filtrado que creas oportuna. Yo por ejemplo considero las excepciones del tipo ModelException de negocio y no quiero llenar mi log de errores de este tipo. Otro ejemplo que suele escribir a veces es no registrar nada si estoy en depuración:

if (HttpContext.Current != null && HttpContext.Current.IsDebuggingEnabled)

{

    return true;

}

Ahora crearemos un nuevo atributo llamado HandleErrorWithElmahAttribute que sustituirá al atributo HandleError.

using System;

using System.Web;

using System.Web.Mvc;
using Models;

using Elmah;

 

namespace MvcApplication1

{

    public class HandleErrorWithElmahAttribute : HandleErrorAttribute

    {       

        public override void OnException(ExceptionContext context)

        {

            var e = context.Exception;

            var httpContext = context.HttpContext;

            if (e is ModelException

                && httpContext.IsCustomErrorEnabled

                && httpContext.Request.IsAjaxRequest())

            {

                var response = httpContext.Response;

                response.StatusCode = (int)HttpStatusCode.InternalServerError;

                response.ContentType = "text/plain";

                response.Write(e.Message);

                context.ExceptionHandled = true;

                return;

            }

            base.OnException(context);

            if (!context.ExceptionHandled

                || RaiseErrorSignal(e)

                || IsFiltered(context))

            {

                return;

            }

            LogException(e);

        }

        private static bool RaiseErrorSignal(Exception e)

        {

            var context = HttpContext.Current;

            if (context == null)

                return false;

            var signal = ErrorSignal.FromContext(context);

            if (signal == null)

                return false;

            signal.Raise(e, context);

            return true;

        }

 

        private static bool IsFiltered(ExceptionContext context)

        {

            var config = context.HttpContext.GetSection("elmah/errorFilter") as ErrorFilterConfiguration;

            if (config == null)

                return false;

            var testContext = new ErrorFilterModule.AssertionHelperContext(context.Exception, HttpContext.Current);

            return config.Assertion.Test(testContext);

        }

 

        private static void LogException(Exception e)

        {

            if (ElmahUtilities.Dismiss(e))

            {

                return;

            }

            var context = HttpContext.Current;

            ErrorLog.GetDefault(context).Log(new Error(e, context));

        }

 

    }

}

Este código es casi el mismo que el del post Logging in MVC Part 1- Elmah. La verdad es que ese post es sólo la primera parte de una serie de post sobre logging en MVC que están geniales, te recomiendo su lectura.

También hay que dar el cambiazo de HandleError por HandleErrorWithElmahAttribute en el fichero FilterConfig.cs.

public static void RegisterGlobalFilters(GlobalFilterCollection filters)

{

    filters.Add(new HandleErrorWithElmahAttribute());

}

Por último hay que agregar manejadores para los eventos ErrorLog_Filtering y ErrorMail_Filtering en el fichero global.asax

        private void ErrorLog_Filtering(object sender, ExceptionFilterEventArgs e)

        {

            if (ElmahUtilities.Dismiss(e.Exception))

            {

                e.Dismiss();

            }

        }

 

        private void ErrorMail_Filtering(object sender, ExceptionFilterEventArgs e)

        {

            if (ElmahUtilities.Dismiss(e.Exception))

            {

                e.Dismiss();

            }

        }

Después de haber tirado todo este código ELMAH ya estará funcionando, pero ¿Qué es importante comentar al respecto de este código?

Lo primero es recordar que ELMAH considera que tiene registrar un error sólo si no ha sido manejado. Si no hubiéramos creado nuestro propio atributo para sustituir HandleError, en producción y con CustomErrors igual a On, HandleError hubiera manejado los errores (ExceptionHandled a true) y ELMAH no registraría ningún error. El resultado sería que el usuario tendría una bonita vista de error y nosotros como desarrolladores no tendríamos nada… mal asunto. Siendo así, ahora se entiende el porqué del nuevo atributo HandleErrorWithElmah.

Lo más importante que hay que entender de HandleErrorWithElmah es la lógica detrás del método OnException.

        public override void OnException(ExceptionContext context)

        {

            var e = context.Exception;

            var httpContext = context.HttpContext;

            if (e is ModelException

                && httpContext.IsCustomErrorEnabled

                && httpContext.Request.IsAjaxRequest())

            {

                var response = httpContext.Response;

                response.StatusCode = (int)HttpStatusCode.InternalServerError;

                response.ContentType = "text/plain";

                response.Write(e.Message);

                context.ExceptionHandled = true;

                return;

            }

            base.OnException(context);

            if (!context.ExceptionHandled

                || RaiseErrorSignal(e)

                || IsFiltered(context))

            {

                return;

            }

            LogException(e);

        }

Lo primero es controlar un caso especial cuando el tipo de la excepción es ModelException, está activados los errores personalizados y además la petición es Ajax. En este caso, no quiero devolver la página de error personalizada porque al ser una excepción de negocio quiero mostrar el mensaje al usuario y quiero ser devolviendo un código de error 500 para poder interceptarlo en el método fail de una promise de jQuery.

Después se llama a la implementación de clase base, es decir, HandleError. Aquí lo queremos es olvidarnos de vistas de error, de información asociada, etc. Eso es un trabajo indigno para ELMAH, que lo haga otro! :)

Después se hace el siguiente razonamiento:

  • Si la excepción ha sido manejada por HandleError, se va a intentar registrar manualmente el error con el método RaiseErrorSignal. Recuerda que si ha sido manejada ELMAH no la registrará automáticamente, así que hay que llamar al método RaiseErrorSignal para hacerlo manualmente.
    • Si por algún motivo RaiseErrorSignal no pudiera registrar el error, primero se comprueba si el error debe ser registrado según el filtro por configuración (elmah/errorFilter). Si no hay sección o no es filtrado, terminaremos registrando el error de forma más manual si cabe, con el método LogException.
  • Si la excepción no ha sido manejada, simplemente dejaremos fluir el asunto y se registrará automáticamente por su cauce normal, que es llamando a los eventos de global.asax.

¿Y dónde entran los eventos de filtrado del global.asax?

Pues a ellos se llegará siempre y cuando no registre el error con LogException. Es decir, LogException no lanza los eventos ErrorLog_Filtering y ErrorMail_Filtering. Sin embargo, el método RaiseErrorSignal o un error no manejado por HandleError, sí llama a esos eventos. Es por esto que se llama manualmente a ElmahUtilities.Dismiss desde LogException (sino lo hiciéramos no podríamos filtrar la excepción).

A propósito de LogException también cabe mencionar que no enviará el error por correo electrónico.

¿Y puedo registrar errores manualmente desde cualquier otro punto de mi aplicación?

Pues se puede, yo utilizo una clase como la siguiente en el ensamblado Models.

using System;

using System.Web;

using Elmah;

 

namespace Models

{

    public static class ErrorLogger

    {

        public static string ApplicationName { get; set; }

 

        public static void Log(Exception ex)

        {

            if (HttpContext.Current != null)

            {

                var errorSignal = ErrorSignal.FromCurrentContext();

                errorSignal.Raise(ex);

            }

            else

            {

                if (ElmahUtilities.Dismiss(ex))

                {

                    return;

                }

                var errorLog = ErrorLog.GetDefault(null);

                errorLog.ApplicationName = ApplicationName;

                errorLog.Log(new Error(ex));

            }

        }

    }

}


En el método Log se explorarán 2 posibles vías:

  • Si hay contexto Http, se utilizará ErrorSignal que lanzará los eventos de global.asax con normalidad.
  • Si no hay contexto (y al igual que sucedía con el método LogException de HandleErrorWithElmah) guardaremos el error pero no lanzará eventos del global.asax ni se enviará correo.

Además, podrás ver que hay una propiedad pública en ErrorLogger llamada ApplicationName. Es un caso extraño. Si no hay contexto (luego el error lo registraremos de forma manual, no con ErrorSignal), la propiedad ApplicationName está vacía. Esto no impide que el error se registre, pero cuando después naveguemos a elmah.axd los errores sin valor en el campo ApplicationName no aparecerán en esa página. Yo por eso dejo preparado ErrorLogger con una propiedad ApplicationName para que pueda establecerse desde fuera si es preciso.

Sólo espero que te sirve el post para poner en marcha ELMAH en ASP.NET MVC e invitarte a que comentes en el blog cualquier cosa que pueda ser de ayuda para todos.

Un saludo!