Caso de éxito: Mejor depuración de Angular con Herramientas para desarrolladores

Una experiencia de depuración mejorada

En los últimos meses, el equipo de Chrome DevTools colaboró con el equipo de Angular para lanzar mejoras en la experiencia de depuración de Chrome DevTools. Las personas de ambos equipos trabajaron juntas y tomaron medidas para permitir que los desarrolladores depuraran y generaran perfiles de aplicaciones web desde la perspectiva de autoría: en términos de su lenguaje fuente y estructura del proyecto, con acceso a información que les resulte familiar y relevante.

En esta publicación, se analizan los cambios que se realizaron en Angular y Chrome DevTools para lograr esto. Aunque algunos de estos cambios se demuestran a través de Angular, también se pueden aplicar a otros frameworks. El equipo de Chrome DevTools alienta a otros frameworks a adoptar las nuevas APIs de consola y los puntos de extensión del mapa de origen para que también puedan ofrecer una mejor experiencia de depuración a sus usuarios.

Código de lista de elementos ignorados

Cuando se depuran aplicaciones con las Herramientas para desarrolladores de Chrome, los autores suelen querer ver solo su código, no el del framework subyacente ni alguna dependencia escondida en la carpeta node_modules.

Para lograrlo, el equipo de DevTools presentó una extensión para los mapas de origen, llamada x_google_ignoreList. Esta extensión se usa para identificar fuentes de terceros, como el código del framework o el código generado por el empaquetador. Cuando un framework usa esta extensión, los autores ahora evitan automáticamente el código que no quieren ver o recorrer sin tener que configurarlo de forma manual de antemano.

En la práctica, Chrome DevTools puede ocultar automáticamente el código identificado como tal en los seguimientos de pila, el árbol de Sources, el diálogo Quick Open y también mejorar el comportamiento de paso y reanudación en el depurador.

GIF animado que muestra DevTools antes y después. Observa cómo, en la imagen posterior, DevTools muestra el código creado por el autor en el árbol, ya no sugiere ninguno de los archivos del framework en el menú "Abrir rápidamente" y muestra un seguimiento de pila mucho más limpio a la derecha.

La extensión de mapa de origen x_google_ignoreList

En los mapas de origen, el nuevo campo x_google_ignoreList hace referencia al array sources y enumera los índices de todas las fuentes de terceros conocidas en ese mapa de origen. Cuando analice el mapa de origen, Chrome DevTools lo usará para determinar qué secciones del código deben incluirse en la lista de elementos que se deben ignorar.

A continuación, se muestra un mapa de origen para un archivo generado out.js. Hay dos sources originales que contribuyeron a generar el archivo de salida: foo.js y lib.js. El primero es algo que escribió un desarrollador de sitios web y el segundo es un framework que usó.

{
  "version" : 3,
  "file": "out.js",
  "sourceRoot": "",
  "sources": ["foo.js", "lib.js"],
  "sourcesContent": ["...", "..."],
  "names": ["src", "maps", "are", "fun"],
  "mappings": "A,AAAB;;ABCDE;"
}

Se incluye sourcesContent para ambas fuentes originales, y Chrome DevTools mostrará estos archivos de forma predeterminada en el depurador:

  • Como archivos en el árbol de fuentes.
  • Como resultados en el cuadro de diálogo Abrir rápido.
  • Como ubicaciones de marco de llamada asignadas en los seguimientos de pila de errores mientras se detiene en un punto de interrupción y se avanza.

Hay un dato adicional que ahora se puede incluir en los mapas de origen para identificar cuál de esas fuentes es código propio o de terceros:

{
  ...
  "sources": ["foo.js", "lib.js"],
  "x_google_ignoreList": [1],
  ...
}

El nuevo campo x_google_ignoreList contiene un solo índice que hace referencia al array sources: 1. Esto especifica que las regiones asignadas a lib.js son, de hecho, código de terceros que se debe agregar automáticamente a la lista de elementos ignorados.

En un ejemplo más complejo, que se muestra a continuación, los índices 2, 4 y 5 especifican que las regiones asignadas a lib1.ts, lib2.coffee y hmr.js son código de terceros que se debe agregar automáticamente a la lista de elementos ignorados.

{
  ...
  "sources": ["foo.html", "bar.css", "lib1.ts", "baz.js", "lib2.coffee", "hmr.js"],
  "x_google_ignoreList": [2, 4, 5],
  ...
}

Si eres desarrollador de frameworks o de empaquetadores, asegúrate de que los mapas de origen generados durante el proceso de compilación incluyan este campo para conectar estas nuevas funciones en Chrome DevTools.

x_google_ignoreList en Angular

A partir de la versión 14.1.0 de Angular, el contenido de las carpetas node_modules y webpack se marcó como “para ignorar”.

Esto se logró a través de un cambio en angular-cli mediante la creación de un complemento que se conecta al módulo Compiler de webpack.

El complemento de webpack que crearon nuestros ingenieros establece hooks en la etapa PROCESS_ASSETS_STAGE_DEV_TOOLING y completa el campo x_google_ignoreList en los mapas de origen para los activos finales que genera webpack y que carga el navegador.

const map = JSON.parse(mapContent) as SourceMap;
const ignoreList = [];

for (const [index, path] of map.sources.entries()) {
  if (path.includes('/node_modules/') || path.startsWith('webpack/')) {
    ignoreList.push(index);
  }
}

map[`x_google_ignoreList`] = ignoreList;
compilation.updateAsset(name, new RawSource(JSON.stringify(map)));

Seguimientos de pila vinculados

Los seguimientos de pila responden a la pregunta “cómo llegué aquí”, pero a menudo esto se hace desde la perspectiva de la máquina y no necesariamente coincide con la perspectiva del desarrollador o su modelo mental del entorno de ejecución de la aplicación. Esto es especialmente cierto cuando algunas operaciones están programadas para que se realicen de forma asíncrona más adelante: aún podría ser interesante conocer la "causa raíz" o el lado de programación de esas operaciones, pero eso es exactamente algo que no será parte de un seguimiento de pila asíncrono.

V8 tiene internamente un mecanismo para hacer un seguimiento de esas tareas asíncronas cuando se usan primitivas de programación estándar del navegador, como setTimeout. Esto se hace de forma predeterminada en esos casos, por lo que los desarrolladores ya pueden inspeccionarlos. Sin embargo, en proyectos más complejos, no es tan simple, especialmente cuando se usa un framework con mecanismos de programación más avanzados, por ejemplo, uno que realiza un seguimiento de zonas, una cola de tareas personalizada o que divide las actualizaciones en varias unidades de trabajo que se ejecutan con el tiempo.

Para solucionar este problema, DevTools expone un mecanismo llamado "API de etiquetado de pila asíncrona" en el objeto console, que permite a los desarrolladores de frameworks sugerir las ubicaciones en las que se programan las operaciones y en las que se ejecutan.

La API de Async Stack Tagging

Sin el etiquetado de pila asíncrona, los seguimientos de pila para el código que los frameworks ejecutan de forma asíncrona de maneras complejas aparecen sin conexión con el código en el que se programó.

Un seguimiento de pila de algún código ejecutado de forma asíncrona sin información sobre cuándo se programó. Solo muestra el seguimiento de pila a partir de "requestAnimationFrame", pero no contiene información de cuándo se programó.

Con el etiquetado de pila asíncrono, es posible proporcionar este contexto, y el seguimiento de pila se ve de la siguiente manera:

Un seguimiento de pila de algún código ejecutado asíncrono con información sobre cuándo se programó. Observa que, a diferencia de antes, incluye "businessLogic" y "schedule" en el seguimiento de pila.

Para lograrlo, usa un nuevo método console llamado console.createTask() que proporciona la API de Async Stack Tagging. Su firma es la siguiente:

interface Console {
  createTask(name: string): Task;
}

interface Task {
  run<T>(f: () => T): T;
}

Invocar console.createTask() muestra una instancia de Task que puedes usar más adelante para ejecutar el código asíncrono.

// Task Creation
const task = console.createTask(name);

// Task Execution
task.run(f);

Las operaciones asíncronas también se pueden anidar, y las "causas raíz" se mostrarán en el seguimiento de pila en secuencia.

Las tareas se pueden ejecutar cualquier cantidad de veces, y la carga útil de trabajo puede diferir entre cada ejecución. La pila de llamadas en el sitio de programación se recordará hasta que se recopile el objeto de tarea.

La API de Async Stack Tagging en Angular

En Angular, se realizaron cambios en NgZone, el contexto de ejecución de Angular que persiste en las tareas asíncronas.

Cuando se programa una tarea, se usa console.createTask() cuando está disponible. La instancia Task resultante se almacena para su uso posterior. Cuando se invoque la tarea, NgZone usará la instancia Task almacenada para ejecutarla.

Estos cambios se implementaron en NgZone 0.11.8 de Angular a través de las solicitudes de extracción #46693 y #46958.

Marcos de llamadas amigables

Los frameworks suelen generar código de todo tipo de lenguajes de plantillas cuando se compila un proyecto, como las plantillas de Angular o JSX que convierten el código con aspecto de HTML en JavaScript simple que, en última instancia, se ejecuta en el navegador. A veces, a este tipo de funciones generadas se les asignan nombres que no son muy amigables, ya sea nombres de una sola letra después de que se comprimen o algunos nombres oscuros o desconocidos, incluso cuando no lo son.

En Angular, no es raro ver marcos de llamadas con nombres como AppComponent_Template_app_button_handleClick_1_listener en los seguimientos de pila.

Captura de pantalla del seguimiento de pila con un nombre de función generado automáticamente.

Para solucionar este problema, Chrome DevTools ahora admite cambiar el nombre de estas funciones a través de mapas de origen. Si un mapa de origen tiene una entrada de nombre para el inicio del alcance de una función (es decir, el paréntesis izquierdo de la lista de parámetros), el marco de llamada debe mostrar ese nombre en el seguimiento de pila.

Marcos de llamadas amigables en Angular

El cambio de nombre de los marcos de llamadas en Angular es un esfuerzo continuo. Esperamos que estas mejoras se implementen gradualmente con el tiempo.

Mientras analiza las plantillas HTML que escribieron los autores, el compilador de Angular genera código TypeScript, que finalmente se transpila en código JavaScript que el navegador carga y ejecuta.

Como parte de este proceso de generación de código, también se crean mapas de origen. Actualmente, estamos explorando formas de incluir nombres de funciones en el campo "names" de los mapas de origen y hacer referencia a esos nombres en las asignaciones entre el código generado y el código original.

Por ejemplo, si se genera una función para un objeto de escucha de eventos y su nombre no es amigable o se quita durante la reducción, los mapas de origen ahora pueden incluir el nombre más amigable para esta función en el campo "names" y la asignación para el comienzo del alcance de la función ahora puede hacer referencia a este nombre (es decir, el paréntesis izquierdo de la lista de parámetros). Luego, Chrome DevTools usará estos nombres para cambiar el nombre de los marcos de llamadas en los seguimientos de pila.

Con la mirada puesta en el futuro

Usar Angular como prueba piloto para verificar nuestro trabajo fue una experiencia maravillosa. Nos encantaría recibir comentarios de los desarrolladores de frameworks y que nos envíen sus comentarios sobre estos puntos de extensión.

Hay más áreas que nos gustaría explorar. En particular, cómo mejorar la experiencia de generación de perfiles en DevTools.