Test de disponibilidad con Node.js y Playwright para Application Insights

Uno de los escenarios en el que he tenido que trabajar en los últimos días es en la creación de tests de disponibilidad personalizados, con el fin de comprobar que una web sigue activa, incluso cuando no haya usuarios pululando. Para realizar esta tarea he utilizado Node.js como lenguaje de programación y la librería de Playwright para simular la navegación necesaria que compruebe que todo está funcionando correctamente:

Arquitectura usada para la PoC

La aplicación web a monitorizar

En este ejemplo estoy aprovechando el escenario del artículo anterior, donde tengo una aplicación que para acceder necesito autenticarme con Azure Active Directory. Si quieres emular el mismo el escenario basta con crear una web app, habilitar la autenticación express a través de tu tenant de Azure Active Directory. Lo único que debes tener en cuenta es que necesitas un usuario que no tenga habilitado un segundo factor de autenticación para este ejemplo. Una vez que tienes esto, si inicias sesión en esta aplicación web verás esta página por defecto:

Página por defecto al crear una web app en Azure App Service

El test con Node.js y Playwright

Ahora que ya tenemos qué probar lo siguiente que necesitas es el test. Este podría estar ubicado incluso en una aplicación de consola, como vimos en el ejemplo anterior. Para este ejemplo he utilizado Azure Functions con un trigger de tipo Timer:

const { chromium } = require('playwright-chromium');
const appInsights = require("applicationinsights");
const { v4 } = require('uuid');
var telemetryClient = new appInsights.TelemetryClient(process.env.APP_INSIGHTS_WEB);
const Stopwatch = require('statman-stopwatch');

module.exports = async function(context, myTimer) {
    var timeStamp = new Date().toISOString();

    if (myTimer.isPastDue) {
        context.log('JavaScript is running late!');
    }
    context.log('JavaScript timer trigger function ran!', timeStamp);

    //Create availability telemetry
    var availabilityTelemetry = {
        id: v4(),
        name: process.env.AVAILABILITY_TEST_NAME,
        runLocation: process.env.RUN_LOCATION,
        success: false
    };

    const stopwatch = new Stopwatch();
    const browser = await chromium.launch({ headless: true });

    try {
        //Create playwright flow        
        const page = await browser.newPage();
        stopwatch.start();

        context.log(`Navigating to: ${process.env.WEB_URL}`);
        await page.goto(process.env.WEB_URL);
        context.log(`Put the email ${process.env.TEST_USER_EMAIL}`);
        await page.type("input[type='email']", process.env.TEST_USER_EMAIL);
        await page.click("input[type='submit']");
        await page.waitForNavigation();

        context.log('Put the password');
        await page.click("[placeholder='Password']");
        await page.type("input[name='passwd']", process.env.TEST_USER_PWD);
        await page.click("input[type='submit']");
        await page.waitForNavigation();

        context.log('Say no');
        await page.click("text=No");
        await page.waitForNavigation();

        var title = await page.title();
        context.log('Title: ' + title);
        availabilityTelemetry.success = true;

    } catch (error) {
        context.log.error('Error: ' + error);
        availabilityTelemetry.success = false;

        //Create exception telemetry        
        telemetryClient.trackException({ exception: error });

    } finally {
        stopwatch.stop();
        var elapsed = stopwatch.read();
        context.log('Stopwatch: ' + elapsed);
        availabilityTelemetry.duration = elapsed;
        telemetryClient.trackAvailability(availabilityTelemetry);
        await browser.close();
        context.done();
    }
}

En él lo que hago es cada 1 minuto ejecuto mi test con Playwright, intento iniciar sesión con el usuario y contraseña que obtenido de las variables de entorno (lo ideal sería almacenarlo en Azure Key Vault) y finalmente obtengo el título de la página, que debería de ser el mismo que el de la página de ejemplo. Si se ha podido avanzar por todos los pasos sin dar timeout (porque no encuentra el control por ejemplo) significa que se ha podido ejecutar todo sin problemas y por lo tanto el test está OK. En caso contrario lo marcará como que no ha pasado y generará además una excepción en el mismo Application Insights.

Para desplegar este ejemplo lo he hecho con Visual Studio Code, pero he creado y configurado las Azure Functions con Azure CLI para añadirle también las App Settings correspondientes:

#Variables
#for the Azure Function
TEST_USER="<AZURE_AD_USER_EMAIL>"
TEST_SECRET="<AZURE_AD_USER_PASSWORD>"
WEB_TO_TEST="<THE_WEB_YOU_WANT_TO_TEST>"
APP_INSIGHTS_TO_REPORT="<APP_INSIGHTS_INSTRUMENTATION_KEY>"
AVAILABILITY_TEST_NAME="Test using Node.js"

#for Azure resources
RESOURCE_GROUP="Custom-Tests"
LOCATION="West Europe"
AZURE_FUNCTION_NAME="app-insights-availability-west"
AZURE_FUNCTION_PLAN="AzFuncNodejsPlan"
STORAGE_ACCOUNT_NAME="playwrightnodejswest"

#1. Create Resource Group
az group create --name $RESOURCE_GROUP --location $LOCATION

#2. Create Storage account
az storage account create --resource-group $RESOURCE_GROUP --name $STORAGE_ACCOUNT_NAME --location $LOCATION --sku Standard_LRS

#Get Azure Storage connection string
STORAGE_CONNECTION_STRING=$(az storage account show-connection-string --name $STORAGE_ACCOUNT_NAME --resource-group $RESOURCE_GROUP --output tsv)

#Create Azure Function Plan
az functionapp plan create --resource-group $RESOURCE_GROUP --name $AZURE_FUNCTION_PLAN --location $LOCATION --number-of-workers 1 --sku EP1 --is-linux

# Create Azure Function
az functionapp create --name $AZURE_FUNCTION_NAME --functions-version 3 --storage-account $STORAGE_ACCOUNT_NAME --resource-group $RESOURCE_GROUP --plan $AZURE_FUNCTION_PLAN  --runtime node --runtime-version 12

#Add app settings to the Azure Function
az functionapp config appsettings set --name $AZURE_FUNCTION_NAME --resource-group $RESOURCE_GROUP \
--settings WEB_URL=$WEB_TO_TEST \
RUN_LOCATION=$LOCATION \
TEST_USER_EMAIL=$TEST_USER \
TEST_USER_PWD=$TEST_SECRET \
APP_INSIGHTS_WEB=$APP_INSIGHTS_TO_REPORT \
AVAILABILITY_TEST_NAME=$AVAILABILITY_TEST_NAME \
PLAYWRIGHT_BROWSERS_PATH=0

Nota: en este ejemplo lo estoy desplegando en Visual Studio Code. Si has creado tu propia Azure Function ten en cuenta que para que Playwright funcione en este servicio debes indicar que quieres que la instalación de las dependencias se hagan en destino. Esto se consigue modificando el archivo .vscode/settings.json

{
    "azureFunctions.deploySubpath": ".",    
    "azureFunctions.projectLanguage": "JavaScript",
    "azureFunctions.projectRuntime": "~3",
    "debug.internalConsoleOptions": "neverOpen",    
    "azureFunctions.scmDoBuildDuringDeployment": true
}

El resultado

Si se ejecuta este test durante un tiempo, en el apartado Availability de Application Insights podrás ver algo como esto:

Resultado de los test personalizados en Application Insights

Como te puedes imaginar, los puntos verdes es que todo va bien, y las X, las cuales he provocado simplemente parando la web app 😉 significa que esa prueba falló. La idea final es que puedas configurar alertas que te avisen cuando las cosas dejen de funcionar. Para ello tienes las siguientes señales a la hora de configurar una alerta:

Señales relacionadas con los Availability tests para las alertas

El código del ejemplo lo tienes en mi GitHub.

¡Saludos!