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 Herramientas para desarrolladores de Chrome colaboró con el equipo de Angular para lanzar mejoras en la experiencia de depuración en Herramientas para desarrolladores de Chrome. Personas de ambos equipos trabajaron en conjunto y tomaron medidas para permitir que los desarrolladores depuren las aplicaciones web y generen perfiles de ellas desde la perspectiva de creación: en términos de su lenguaje de origen y estructura de proyecto, con acceso a información que les resulte familiar y relevante.

En esta publicación, se analiza en detalle qué cambios se requerían para las Herramientas para desarrolladores de Angular y Chrome para lograrlo. Si bien algunos de estos cambios se demuestran a través de Angular, también se pueden aplicar a otros frameworks. El equipo de Herramientas para desarrolladores de Chrome alienta a otros frameworks a adoptar las nuevas API 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.

Ignorar código de ficha

Cuando depuran aplicaciones con las Herramientas para desarrolladores de Chrome, los autores suelen querer ver solo su código, no el del marco de trabajo que contiene o alguna dependencia escondida en la carpeta node_modules.

Para lograr esto, el equipo de Herramientas para desarrolladores introdujo una extensión a 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 agrupadores. Cuando un framework usa esta extensión, los autores ahora evitan automáticamente el código que no quieren ver o revisar sin tener que configurarlo de forma manual con anticipación.

En la práctica, las Herramientas para desarrolladores de Chrome pueden ocultar automáticamente el código identificado como tal en los seguimientos de pila, el árbol de fuentes y el diálogo de apertura rápida, y también mejorar el comportamiento de pasos y reanudación en el depurador.

GIF animado que muestra Herramientas para desarrolladores antes y después. Observa cómo, en la imagen del después, Herramientas para desarrolladores muestra el código de autor en el árbol, ya no sugiere ningún archivo del marco de trabajo en el menú “Inicio rápido” 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 orígenes, 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. Al analizar el mapa de fuentes, las Herramientas para desarrolladores de Chrome lo usarán para determinar qué secciones del código deben ignorarse.

A continuación, se muestra un mapa de fuentes 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 las Herramientas para desarrolladores de Chrome mostrarían estos archivos de forma predeterminada en el depurador:

  • Como archivos en el árbol de fuentes.
  • Como resultados en el diálogo de apertura rápida
  • Como ubicaciones asignadas de los marcos de llamadas en los seguimientos de pila de errores mientras están pausados en un punto de interrupción y mientras caminan.

Hay un dato adicional que ahora se puede incluir en los mapas de origen para identificar cuál de esas fuentes es un 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 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ódigos de terceros que se deben 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 conectarse a estas nuevas capacidades de 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 webpack que crearon nuestros ingenieros se conecta a 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 se 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, a menudo, esto 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 están programadas para realizarse de forma asíncrona más tarde: aún podría ser interesante conocer la “causa raíz” o la programación de esas operaciones, pero eso es algo que no será parte de un seguimiento de pila asíncrono.

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

Para solucionar este problema, las Herramientas para desarrolladores exponen un mecanismo llamado "API de Async Stack Tagging" en el objeto console, que permite a los desarrolladores de frameworks indicar tanto las ubicaciones en las que se programan las operaciones como en las que se ejecutan.

La API de Async Stack Tagging

Sin el etiquetado de pilas asíncronos, los seguimientos de pila del código que los frameworks ejecutan de forma asíncrona y compleja aparecen sin conexión al 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 que comienza en `requestAnimationFrame`, pero no contiene información del momento en que 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:

Seguimiento de pila de algún código asíncrono ejecutado con información sobre cuándo se programó. Observa que, a diferencia de lo anterior, 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;
}

La invocación de 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 cuantas veces quieras y la carga útil de trabajo puede variar entre cada ejecución. La pila de llamadas del sitio de programación se recordará hasta que el objeto de la tarea se recolecte como elemento no utilizado.

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 todas las tareas asíncronas.

Cuando programa 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 Task almacenada para ejecutarla.

Estos cambios llegaron a 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 a partir de todo tipo de lenguajes de plantilla cuando se compila un proyecto, como las plantillas de Angular o JSX que convierten código de aspecto HTML en JavaScript simple que, finalmente, se ejecuta en el navegador. A veces, estos tipos de funciones generadas reciben nombres poco amigables, como nombres con una sola letra después de su reducción o nombres oscuros 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 seguimientos de pila.

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

Para solucionar este problema, las Herramientas para desarrolladores de Chrome ahora admiten el cambio de nombre de estas funciones a través de mapas de origen. Si un mapa de orígenes tiene una entrada de nombre para el inicio del alcance de una función (es decir, el par 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 llamada en Angular es un esfuerzo continuo. Esperamos que estas mejoras se implementen de forma gradual.

Mientras se analizan las plantillas HTML que escriben los autores, el compilador Angular genera código de TypeScript, que finalmente 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 fuentes. 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 fácil de usar 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 ha sido una experiencia maravillosa. Nos encantaría conocer las opiniones de los desarrolladores de frameworks y enviarnos 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 Herramientas para desarrolladores.