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

Una experiencia de depuración mejorada

Durante los últimos meses, el equipo de Herramientas para desarrolladores de Chrome colaboró con el equipo de Angular para lanzar mejoras en la experiencia de depuración de las Herramientas para desarrolladores de Chrome. Los miembros de ambos equipos trabajaron en conjunto y tomaron medidas para permitir a los desarrolladores depurar y generar perfiles de las aplicaciones web desde la perspectiva de la creación en términos de su lenguaje de origen y estructura del proyecto, con acceso a información que les resulta familiar y relevante.

Esta publicación analiza internamente para ver qué cambios se requerían en Angular y las Herramientas para desarrolladores de Chrome para lograrlo. Aunque algunos de estos cambios se demuestran mediante Angular, también se pueden aplicar a otros frameworks. El equipo de Herramientas para desarrolladores de Chrome recomienda a otros marcos de trabajo que adopten las nuevas APIs de la 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 depuras aplicaciones con las Herramientas para desarrolladores de Chrome, por lo general, los autores solo quieren ver solo su código, y no el del framework o alguna dependencia escondida en la carpeta node_modules.

Para lograrlo, el equipo de Herramientas para desarrolladores presentó una extensión para los mapas de fuentes, 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 agrupador. Cuando un framework usa esta extensión, los autores evitan automáticamente el código que no quieren ver ni revisar sin tener que configurarlo de manera manual.

En la práctica, las Herramientas para desarrolladores de Chrome pueden ocultar automáticamente el código que se identifica como tal en los seguimientos de pila, el árbol de fuentes y el diálogo de Apertura rápida, además de mejorar el comportamiento de recorrer y reanudar en el depurador.

GIF animado que muestra Herramientas para desarrolladores antes y después. Observa cómo, en la imagen de después, Herramientas para desarrolladores muestra el código de creación en el árbol, ya no sugiere ningún archivo del framework en el menú “Quick Open” y muestra un seguimiento de pila mucho más limpio a la derecha.

La extensión de mapa de fuentes 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 fuentes. Cuando se analice el mapa de fuentes, las Herramientas para desarrolladores de Chrome lo usarán para determinar qué secciones del código deberían incluirse en la lista de elementos ignorados.

A continuación, se muestra un mapa de fuentes para un archivo out.js generado. 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 utilizaron.

{
  "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 estas dos fuentes originales, y las Herramientas para desarrolladores de Chrome mostrarán estos archivos de forma predeterminada en Debugger:

  • Como archivos del árbol de fuentes
  • Como resultados en el diálogo de Apertura rápida
  • Como ubicaciones asignadas de marcos de llamadas en los seguimientos de pila de errores mientras se pausa en un punto de interrupción y durante el recorrido.

Hay otro 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. De esta manera, se especifica que las regiones asignadas a lib.js son, de hecho, código de terceros que debe agregarse 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 un desarrollador de framework o agrupador, asegúrate de que los mapas de origen generados durante el proceso de compilación incluyan este campo para conectarte con estas nuevas capacidades en Herramientas para desarrolladores de Chrome.

x_google_ignoreList en Angular

A partir de Angular v14.1.0, el contenido de las carpetas node_modules y webpack se marcó como “para ignorar”.

Esto se logró con un cambio en angular-cli mediante la creación de un complemento que se engancha al módulo Compiler de webpack.

El complemento para webpack que crearon nuestros ingenieros hooks en la etapa PROCESS_ASSETS_STAGE_DEV_TOOLING y propaga el campo x_google_ignoreList en los mapas de origen para los recursos finales que genera Webpack y 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 la pregunta “¿cómo llegué aquí”, pero muchas veces es desde la perspectiva de la máquina y no necesariamente es algo que coincide con la perspectiva del desarrollador o su modelo mental del tiempo de ejecución de la aplicación. Esto es especialmente cierto cuando algunas operaciones se programan de forma asíncrona más adelante: aún podría ser interesante conocer la “causa raíz” o el lado de la programación de esas operaciones, pero eso es algo que no será parte de un seguimiento de pila asíncrono.

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

Para solucionar este problema, Herramientas para desarrolladores expone un mecanismo llamado "API de etiquetado de pila asíncrona" en el objeto console, que permite a los desarrolladores de framework indicar las ubicaciones en las que se programan las operaciones y dónde se ejecutan estas operaciones.

La API de etiquetado de pila asíncrona

Sin el etiquetado de pila asíncrona, los seguimientos de pila del código que los frameworks ejecutan de forma asíncrona y compleja de maneras complejas se muestran sin conexión con el código donde se programó.

Seguimiento de pila de algún código asíncrono ejecutado sin información sobre cuándo se programó. Solo muestra el seguimiento de pila a partir de `requestAnimationFrame`, pero no contiene información del momento en que se programó.

Con el etiquetado de pila asíncrona, es posible proporcionar este contexto, y el seguimiento de pila se ve así:

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 etiquetado de pila asíncrona. Su firma es la siguiente:

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

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

Cuando se invoca console.createTask(), se 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 pueden anidarse y las “causas raíz” se mostrarán en secuencia en el seguimiento de pila.

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 el objeto de la tarea se recolecte como elemento no utilizado.

La API de etiquetado de pila asíncrona en Angular

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

Para programar una tarea, 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 de Task almacenada para ejecutarla.

Estos cambios llegaron a NgZone 0.11.8 de Angular a través de solicitudes de extracción #46693 y #46958.

Marcos de llamada amigables

Los frameworks suelen generar código a partir de todo tipo de lenguajes de plantillas cuando se compila un proyecto, como las plantillas de Angular o JSX que convierten el código HTML en JavaScript simple que, finalmente, se ejecutará en el navegador. A veces, este tipo de funciones generadas tienen nombres que no son muy amigables, ya sea nombres con una sola letra después de reducirse o algunos nombres desconocidos o desconocidos, incluso cuando no lo son.

En Angular, es común ver marcos de llamada 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, las Herramientas para desarrolladores de Chrome ahora permiten cambiar el nombre de estas funciones por medio 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 llamada amigables en Angular

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

Al analizar las plantillas HTML que escribieron los autores, el compilador de Angular genera código TypeScript, que eventualmente se transpila a 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 “nombres” 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 descriptivo para esta función en el campo “names”, y la asignación del comienzo del alcance de la función ahora puede referirse a este nombre (es decir, el par izquierdo de la lista de parámetros). Luego, las Herramientas para desarrolladores de Chrome usarán 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 noticias de los desarrolladores del framework y enviarnos comentarios sobre estos puntos de las extensiones.

Hay más áreas que nos gustaría explorar. En particular, la forma de mejorar la experiencia de generación de perfiles en Herramientas para desarrolladores.