Distrito Telefónica. Hub de Innovación y Talento

Ajuste de Sentry para la monitorización del rendimiento del frontend

En el mundo del desarrollo web, ofrecer experiencias de usuario excepcionales y con capacidad de respuesta es la clave del éxito. A medida que las aplicaciones web modernas se vuelven cada vez más sofisticadas, la monitorización de su rendimiento y la identificación de los problemas que afectan a los usuarios se han convertido en aspectos fundamentales del proceso de desarrollo. Existen numerosas herramientas en Internet que pueden ayudarte a mejorar tu experiencia de usuario, pero en este artículo vamos a profundizar en Sentry aplicado a nuestro caso de uso particular. 

En el Checkout Team tenemos un conjunto de flujos de gestión de pagos y métodos de pago (guardar tarjetas de crédito, cuentas bancarias, etc.) diseñados para ser fácilmente integrados por cualquier otra aplicación. Para este asunto, y además de la integración API, disponemos de una integración ligera basada en un flujo web al que se accede a través de un enlace que solo requiere un parámetro de consulta con un JWT que contenga la información a pagar firmada. Cualquier aplicación de terceros dentro del ecosistema de Telefónica que requiera que el usuario realice un pago puede simplemente delegarlo a nuestro flujo y disfrutar de los beneficios de una experiencia de usuario de pago plug-and-play totalmente funcional. Nuestro producto puede cargarse directamente como una página web independiente o inyectarse en el sitio web original dentro de un iframe, lo que permite al sitio que lo incrusta mantener el control total de la experiencia. 

Nuestra aplicación está construida sobre NextJS y, durante mucho tiempo, hemos utilizado Sentry como herramienta para detectar y controlar errores en nuestro código de producción. Pero lo que no teníamos hasta ahora era una forma de hacer un seguimiento de cómo funcionaba nuestra aplicación para el usuario final en lo que respecta al rendimiento. Queríamos saber qué partes de nuestra aplicación podían ralentizar las interacciones para el usuario final y también disponer de registros frontales que pudieran compararse con nuestros registros backend de Kibana. Aquí es donde la monitorización del rendimiento de Sentry resulta muy útil. 

Activación de la monitorización del rendimiento de Sentry

Activar la monitorización del rendimiento de Sentry en NextJS es tan sencillo como establecer la frecuencia de muestreo deseada de los rastros en tus sentry.client.config.ts y sentry.server.config.ts archivos: 

import * as Sentry from "@sentry/nextjs"; 
 
Sentry.init({ 
  tracesSampleRate: 1.0, 
}); 

En nuestro caso, se espera mucho tráfico de usuarios, por lo que queremos reducir la frecuencia de muestreo: 

// sentry.client.config.ts 
import * as Sentry from "@sentry/nextjs"; 
import getConfig from 'next/config'; 
 
Sentry.init({ 
  tracesSampleRate: 0.1, 
}); 

Para nuestra configuración inicial este es todo el código que necesitamos. Fácil, ¿verdad? 

Ahora deberías haber activado la pestaña de rendimiento de tu despliegue de Sentry. Una vez que recibas algunos datos, deberías esperar a ver un panel similar a este: 
Pantalla principal de la pestaña de rendimiento en Sentry

Pantalla principal de la pestaña de rendimiento en Sentry

Más adelante en el artículo explicaremos la información principal que se puede extraer de estos paneles, pero antes tendremos que abordar algunos retoques que tuvimos que aplicar a la configuración inicial para que los datos enviados se agruparan y agregaran correctamente en ellos. 

Hacer visibles los parámetros de las cadenas de consulta

Las transacciones son los objetos principales que agrupan toda la información y los rastros capturados para una interacción de usuario. Aparecen en la pantalla principal de la pestaña Rendimiento Sentry: 
Lista de transacciones

Lista de transacciones

Nuestro flujo principal es una SPA que lleva al usuario a través de un itinerario de pago utilizando un conjunto variable de pasos. Queríamos permitir al usuario (de forma nativa) navegar hacia adelante y hacia atrás a través de nuestro flujo, por lo que tuvimos que realizar un seguimiento del paso actual en alguna parte. También queríamos enrutamiento del lado del cliente y transacciones fácilmente rastreables, lo que nos llevó a utilizar un parámetro de consulta para este asunto. Siguiendo este ejemplo, nuestra URL inicial podría ser: https://oursite.com/payment?webview-step=init. Cuando el usuario haga clic en el botón Continuar, pasará al siguiente paso: https://oursite.com/payment?webview-step=payment-form, etc. Queríamos que este parámetro de consulta estuviera disponible en el nombre de la transacción de Sentry, para poder tener rastros agrupados por el paso que el usuario estaba cargando o por el que estaba navegando. 

Además, también recibimos un parámetro de consulta JWT que contenía información cifrada y firmada sobre el pago. Este JWT se considera información sensible y no debería registrarse en ningún sitio. Teniendo esto en cuenta, un ejemplo de URL inicial podría ser: https://oursite.com/payment?step=init&jwt=ey.iashdfiusaduihUysdbasHFY... Además de ser información sensible, este parámetro de consulta sería diferente para cada interacción, por lo que debería sustituirse por un marcador de posición antes de ser enviado, ya que queríamos que todas las transacciones relacionadas con el mismo paso se agruparan en Sentry. 

Por defecto Sentry elimina los parámetros de consulta de los nombres de las transacciones (que son efectivamente las URL cargadas o hacia las que se navega), por lo que con nuestra configuración inicial todas las páginas serían rastreadas bajo el mismo nombre de transacción(https://oursite.com/payment), y no podríamos ver los detalles de los eventos agrupados por el paso que el usuario está cargando o hacia el que navega. 
Para fijar el nombre de las transacciones y agruparlas según el paso en el que se encuentra el usuario, tuvimos que implementar una integración BrowserTracing personalizada

/ sentry.client.config.ts 
import * as Sentry from "@sentry/nextjs"; 
import {beforeNavigate} from './utils/sentry' 
 
Sentry.init({ 
    // ... 
    integrations: [ 
        new Sentry.BrowserTracing({ 
            beforeNavigate, 
        }), 
    ], 
});// utils/sentry.ts 
// Sentry strips query parameters from the url on navigation events. We get them back by listening to next router. https://github.com/getsentry/sentry-javascript/pull/8278 
let currentNavigationPath: string = 
    typeof window !== 'undefined' ? window.location.pathname + window.location.search : ''; 
let nextNavigationPath: string = 
    typeof window !== 'undefined' ? window.location.pathname + window.location.search : ''; 
 
Router.events.on('routeChangeStart', (navigationTarget: string) => { 
    nextNavigationPath = navigationTarget; 
}); 
 
export const beforeNavigate = (context: TransactionContext): TransactionContext => { 
    context.name = removeJwtFromQueryParams(nextNavigationPath); 
    if (typeof context.tags?.from === 'string') { 
        context.tags.from = removeJwtFromQueryParams(currentNavigationPath); 
    } 
    currentNavigationPath = nextNavigationPath; // Once we have completed the navigation, we can update the current path 
    return context; 
};


Como puedes ver, estamos confiando en eventos del rúter de Next para saber por dónde navega el usuario. A continuación, utilizamos esa información para modificar la transacción que será enviada a Sentry, añadiendo de nuevo los parámetros de consulta que son automáticamente eliminados por Sentry. La función removeJwtFromQueryParams se explicará a continuación. 

Parámetros de URL variables

Por otro lado, también teníamos parámetros variables en nuestros eventos relacionados con la URL que queríamos agrupar. Por ejemplo, la solicitud de API GET /api/pets/1 debería rastrearse en el mismo grupo que GET /api/pets/2. Para ello, debemos sustituir la variable pet id en la ruta por un marcador de posición: GET /api/pets/<pet-id>. Lo mismo se aplica al parámetro de consulta jwt en el punto de entrada de nuestro flujo. Cada token jwt es diferente, pero queríamos agrupar todas las navegaciones a nuestros puntos de entrada bajo los mismos nombres de transacción que incluyen un marcador de posición para el token JWT. Además, como ya se ha mencionado, los tokens JWT contienen información sensible y no deberían registrarse en ningún sitio, por lo que este mecanismo de eliminación de parámetros puede servir para múltiples propósitos, como anonimizar, eliminar datos personales y agrupar transacciones, tramos y migas de pan. 

Sentry ofrece 2 funciones para modificar estos metadatos: beforeBreadcrumb y beforeSendTransaction
Las migas incluyen eventos de navegación, solicitudes XHR, clics del usuario, etc. Se enumeran bajo transacciones particulares y pueden ayudar a rastrear lo que sucedió en una interacción particular del usuario. 
Breadcrumbs

Breadcrumbs

Por otra parte, con respecto a beforeSendTransaction las transacciones son los principales objetos que contienen rastros. En nuestro caso tenemos 2 tipos: cargas de páginas y navegaciones. 
Podemos aprovechar las dos funciones descritas anteriormente para dar formato a nuestros eventos de forma que Sentry los agrupe automáticamente. 

// sentry.client.config.ts 
import * as Sentry from "@sentry/nextjs"; 
import {beforeBreadcrumb, beforeSendTransaction} from './utils/sentry' 
 
Sentry.init({ 
    // ... 
    beforeBreadcrumb, 
    beforeSendTransaction, 
}); 

Queremos normalizar el parámetro de consulta jwt de las migas de navegación. Por otro lado, para traer migas de pan, tenemos parámetros de ruta variable (en nuestras llamadas http a la API) que también deberían eliminarse: 

 

export const beforeBreadcrumb = (breadcrumb: Sentry.Breadcrumb): Sentry.Breadcrumb => { 
    if (breadcrumb.category === 'navigation') { 
        breadcrumb.data.from = removeJwtFromQueryParams(breadcrumb.data.from); 
        breadcrumb.data.to = removeJwtFromQueryParams(breadcrumb.data.to); 
        return breadcrumb; 
    } 
    if (breadcrumb.category === 'fetch') { 
        breadcrumb.data.url = removeJwtFromApiPath(breadcrumb.data.url); 
        return breadcrumb; 
    } 
    return breadcrumb; 
}; 


Para las transacciones, queremos eliminar el parámetro de consulta jwt del objeto de solicitud. También queremos iterar a través de los tramos y normalizar los que representan llamadas http a nuestra API: 

export const beforeSendTransaction = (transaction: TransactionEvent): TransactionEvent => { 
    if (transaction?.request?.url) { 
        transaction.request.url = removeJwtFromQueryParams(transaction.request.url); 
    } 
    if (transaction.request?.headers?.Referer) { 
        transaction.request.headers.Referer = removeJwtFromQueryParams(transaction.request.headers.Referer); 
    } 
    transaction.spans = transaction.spans?.map(parameterizeHttpRequests); 
    return transaction; 
}; 
const parameterizeHttpRequests = (span: Span): Span => { 
    if (span.op === 'http.client') { 
        if (span.data.url) { 
            span.data.url = removeJwtFromApiPath(span.data.url); 
        } 
        if (span.description) { 
            span.description = removeJwtFromApiPath(span.description); 
        } 
    } 
    return span; 
}; 

Las funciones para normalizar las URL pueden adaptarse a tu propia API y a las URL de tus clientes. Por ejemplo, este sustituye un parámetro de consulta JWT con un símbolo de guión: 

const removeJwtFromQueryParams = (urlPath: string) => { 
    if (urlPath.includes('?')) { 
        const [path, queryString] = urlPath.split('?'); 
        const searchParams = new URLSearchParams(queryString); 
        searchParams.set('jwt', '-'); 
        return `${path}?${searchParams.toString()}`; 
    } 
    return urlPath; 
}; 

Implementa como quieras tus propias funciones de sustitución adaptadas a tu caso de uso. 

Resultado final

Una vez aplicados todos los cambios, este es el resultado final. Aquí podemos ver la pestaña principal de rendimiento, donde tenemos algunos datos generales y una lista de los nombres de nuestras transacciones. Disponemos de información sobre el TPM, los tiempos de carga (percentiles) y los índices de error. Como puedes ver, el parámetro de consulta jwt ha sido sustituido por un marcador de posición. 
Resultado final de la pestaña principal de rendimiento

Resultado final de la pestaña principal de rendimiento

Si hacemos clic en una transacción obtendremos información detallada sobre la misma: 
Detalles de la transacción

Detalles de la transacción

Puedes ver que los tramos están agregados y agrupados por transacción. El primer tramo es una solicitud GET XHR cuyo token jwt ha sido sustituido por un marcador de posición. Justo debajo de los percentiles se muestra una lista de las transacciones concretas (las más lentas). Si hacemos clic en uno de esos eventos podemos obtener aún más información sobre esa interacción concreta: 
Detalles del evento

Detalles del evento

Se trata de un evento de carga de página. Podemos ver un gráfico de cascada detallado de los eventos que ocurrieron en el navegador mientras se cargaba la página, incluyendo la conexión http, la descarga de paquetes, las pinturas del navegador, las solicitudes XHR, etc. También podemos ver una lista de las migas de pan disponibles para la interacción, donde el parámetro jwt también ha sido sustituido por un marcador de posición. 

Ahora, con esta herramienta en nuestras manos, podemos empezar a buscar cuellos de botella y posibles problemas de rendimiento que hacen que nuestra aplicación web vaya más lenta. Pero esto es solo un anticipo de las posibilidades de análisis del rendimiento que puede ofrecer Sentry. Para más información consulta documento de Sentry