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 en Herramientas para desarrolladores de Chrome. 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. 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.

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 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 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 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 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ó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 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 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.

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 abordar 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 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 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 programa una tarea, usa console.createTask() cuando está disponible. La instancia de Task resultante se almacena para su uso posterior. Cuando invoques 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, 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 del 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 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 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 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.