wandres.dev
VISUALIZAR · Stately Studio

El inspector: ver el comportamiento mientras ocurre

El diagrama estático dice qué puede pasar; el inspector dice qué está pasando. La lección va más allá de la instrumentación básica que introdujo el nivel 3 y trata el flujo de eventos de inspección como una infraestructura de observabilidad: `createBrowserInspector` y sus opciones, el inspector por WebSocket para el servidor y para dispositivos, la vista del árbol de actores completo, y sobre todo la traza como artefacto de primera clase que se graba, se reproduce, se adjunta a un informe de error y se usa como oráculo de pruebas de extremo a extremo.

⏱ 19 min

Depurar una interfaz consiste, casi siempre, en reconstruir a posteriori una historia que ya ocurrió: qué hizo el usuario, en qué orden, qué respondió el servidor y en qué punto exacto el sistema entró en el estado que produjo la pantalla rota. Los registros de consola son un intento pobre de contar esa historia porque están sembrados a mano en los sitios donde alguien sospechó que haría falta, y la sospecha nunca coincide con el fallo real. Un actor de XState no necesita que nadie siembre nada: emite, por construcción, un evento observable por cada mensaje que recibe y por cada transición que ejecuta. El inspector es lo que ocurre cuando alguien decide consumir ese flujo bien, pero lo importante de esta lección no es la herramienta visual, sino aprender a tratar el flujo como lo que es: la historia completa y ordenada del comportamiento, disponible sin coste de instrumentación.

🎯 Al terminar esta lección sabrás
  • Configurar createBrowserInspector con filtrado, arranque diferido y transformación de eventos antes de emitirlos.
  • Inspeccionar procesos sin navegador mediante el inspector por WebSocket, en servidor, en dispositivo o en pruebas.
  • Leer la vista del árbol de actores y de la secuencia de mensajes como diagnóstico de sistemas con invoke y spawn.
  • Grabar la traza de una sesión, reproducirla contra la máquina y usarla como oráculo de pruebas.

Del registro sembrado al diagrama vivo

La opción inspect de createActor acepta una función que recibe cada evento de inspección del sistema. createBrowserInspector no es más que un consumidor bien hecho de ese flujo que abre una vista en el navegador y anima el diagrama, pero conviene conocer sus opciones porque son las que separan una herramienta usable de una que se vuelve inservible al segundo minuto de uso real.

import { createActor } from 'xstate'
import { createBrowserInspector } from '@statelyai/inspect'
import { maquina } from './maquina'

const inspector = createBrowserInspector({
  // No abre la vista hasta que se llama a su metodo start.
  autoStart: false,
  // Descarta el ruido interno: temporizadores, resoluciones de invoke, etc.
  filter: (ev) => {
    if (ev.type !== '@xstate.event') return true
    return !ev.event.type.startsWith('xstate.')
  },
})

const actor = createActor(maquina, { inspect: inspector.inspect })
actor.start()

document.querySelector('#depurar')?.addEventListener('click', () => inspector.start())

Tres decisiones merecen comentario. autoStart en falso evita que se abra una pestaña cada vez que alguien recarga la aplicación en desarrollo, lo cual parece un detalle y es la diferencia entre que el equipo use el inspector o lo desactive el primer día. El filter es imprescindible en cuanto la máquina tiene invoke o transiciones retardadas, porque los eventos internos con prefijo xstate. inundan la vista y ocultan los eventos de dominio, que son los únicos que un humano puede interpretar. Y existe además una opción de serialización que permite transformar el evento antes de emitirlo, cuyo uso principal es tapar datos sensibles del contexto; conviene consultar los tipos de la versión instalada, porque su firma ha cambiado entre iteraciones del paquete.

💡
Inspeccionar no altera lo inspeccionado, pero sí cuesta

La instrumentación es pasiva: no modifica transiciones ni introduce efectos. Lo que sí introduce es trabajo síncrono en cada transición —construir el evento, filtrarlo, serializar el snapshot y enviarlo por un canal— y ese trabajo escala con el tamaño del contexto. En una máquina cuyo contexto guarda una lista de mil elementos, serializar en cada transición es perfectamente capaz de dominar el perfil de rendimiento. Filtra pronto, no tarde, y si el contexto es grande, emite solo la parte que vas a mirar.

Sin navegador: WebSocket, servidor y dispositivo

El inspector de navegador resuelve el caso de la aplicación web en desarrollo, que no es el único ni el más difícil. Una máquina que orquesta un proceso en el servidor, una aplicación nativa con React Native o una suite de pruebas que corre sin interfaz necesitan el mismo flujo por otro canal, y para eso está el inspector por WebSocket: el proceso instrumentado actúa como emisor y la vista se conecta como consumidor, de modo que el diagrama vivo puede estar en una máquina distinta de la que ejecuta el actor.

import { createActor } from 'xstate'
import { createWebSocketInspector } from '@statelyai/inspect'
import { flujoDePedido } from './flujo-de-pedido'

const inspector = createWebSocketInspector({ url: 'ws://localhost:8080' })
inspector.start()

const actor = createActor(flujoDePedido, { inspect: inspector.inspect })
actor.start()
flowchart LR
A[Actor raiz] -->|eventos de inspeccion| B[Funcion inspect]
B --> C[Inspector de navegador]
B --> D[Inspector por WebSocket]
B --> E[Registro propio o panel interno]
B --> F[Oraculo de pruebas]
G[Actores hijos por invoke o spawn] -->|suben al raiz| B

Ese diagrama contiene el hecho estructural más importante de la lección y conviene subrayarlo: la inspección es del sistema de actores completo, no de una máquina aislada. El actor raíz recibe los eventos de inspección de todos sus descendientes, así que instrumentar la raíz basta para ver nacer cada actor invocado, cada mensaje que cruza entre padre e hijo y cada snapshot de cualquier rama del árbol. Por eso el inspector visual puede mostrar, junto al diagrama de estados, una vista de secuencia con los mensajes entre actores, que es la única forma legible de depurar un sistema donde varias máquinas se coordinan. Cuando el fallo consiste en que un hijo respondió tarde o en que un padre envió un mensaje al actor equivocado, el diagrama de estados no lo muestra y la secuencia sí.

ℹ️
Los tres eventos y sus campos útiles

El flujo consta esencialmente de @xstate.actor cuando un actor nace, @xstate.event cuando recibe un mensaje y @xstate.snapshot tras cada transición. Para razonar sobre sistemas con varios actores, los campos que importan no son solo el evento o el snapshot, sino la identidad: actorRef señala a quién le pasa, sourceRef señala quién lo envió y rootId identifica la sesión completa. Filtrar por identidad es lo que permite aislar el comportamiento de una submáquina en un sistema con decenas de actores vivos.

La traza como artefacto

Aquí está el salto conceptual del nivel. Si se guardan en orden los eventos de dominio que recibió el actor raíz durante una sesión, se obtiene una estructura de datos pequeña, serializable y legible que describe por completo lo que el usuario hizo. Y como el intérprete es determinista —dada la misma definición, el mismo contexto inicial y la misma secuencia de eventos, la trayectoria de estados es siempre la misma—, esa lista no es un registro descriptivo sino un programa reproducible. Se puede adjuntar a un informe de error, guardar en una prueba, enviar desde producción o pegar en el estudio para recorrerla.

import { createActor } from 'xstate'
import { maquina } from './maquina'

type Traza = Array<{ type: string; [k: string]: unknown }>

export function grabar() {
  const traza: Traza = []
  const actor = createActor(maquina, {
    inspect: (ev) => {
      if (ev.type !== '@xstate.event') return
      if (ev.event.type.startsWith('xstate.')) return
      traza.push(ev.event)
    },
  })
  return { actor, traza }
}

export function reproducir(traza: Traza) {
  const copia = createActor(maquina)
  copia.start()
  for (const evento of traza) copia.send(evento as never)
  return copia.getSnapshot()
}

La reproducción es fiel bajo dos condiciones que conviene enunciar sin ambigüedad, porque su incumplimiento produce reproducciones engañosas que cuestan horas. La primera es que los actores invocados sean deterministas o estén sustituidos por dobles: si la máquina llama a una interfaz remota, el resultado de la llamada forma parte de la entrada y hay que grabarlo junto a los eventos o inyectar una implementación falsa con el método provide. La segunda es el tiempo: las transiciones retardadas dependen del reloj, así que una reproducción honesta usa un reloj simulado en lugar del real, exactamente igual que en las pruebas del nivel anterior. Con esas dos condiciones cubiertas, la traza reproduce el fallo en un test de milisegundos.

🔍

Depurar

Reproducir en local, paso a paso, la sesión que falló, en lugar de reconstruirla desde una descripción escrita por quien la sufrió.

📎

Adjuntar al informe

El informe de error deja de ser una narración y pasa a ser un dato ejecutable que reproduce el fallo en la primera ejecución.

🛡️

Fijar la regresión

La traza que provocó el fallo se convierte, sin escribir nada, en el caso de prueba que impide que vuelva a ocurrir.

📊

Medir el producto

Los embudos se calculan sobre transiciones reales en vez de sobre eventos de analítica sembrados a mano y siempre desfasados.

La cuarta tarjeta merece desarrollo porque suele sorprender. La analítica de producto se implementa casi siempre sembrando llamadas de seguimiento en los manejadores de la interfaz, con dos defectos crónicos: miden pulsaciones y no comportamiento, y se desincronizan del código exactamente igual que los diagramas dibujados a mano. Si en cambio se derivan de las transiciones —cuántas sesiones entraron en cobrando, cuántas salieron a fallido, cuánto tiempo mediaron—, el embudo mide el proceso real y se actualiza solo cuando el proceso cambia. Es la misma tesis del nivel aplicada a otro consumidor: dejar de mantener a mano lo que se puede derivar.

El uso más rentable de esta idea no es depurar sino probar. Una prueba de extremo a extremo tradicional afirma sobre el árbol de nodos: que aparezca cierto texto, que un botón esté deshabilitado. Esas afirmaciones son frágiles porque el árbol de nodos es una consecuencia lejana del comportamiento. Si el navegador de pruebas recoge la traza de inspección y la prueba afirma sobre la trayectoria de estados —que se pasó por cobrando y se terminó en hecho sin visitar agotado—, se está afirmando sobre el comportamiento mismo. La interfaz puede rediseñarse entera sin romper la prueba, y en cambio cualquier cambio real del protocolo la rompe, que es justo lo que se quiere de una prueba.

⚠️
Fuera del entorno de desarrollo, con condiciones

Emitir trazas desde producción es tentador y hay que hacerlo con cabeza. El contexto de una máquina de autenticación o de pago contiene con frecuencia identificadores, correos o importes, y enviarlos a un servicio externo convierte una herramienta de depuración en una fuga de datos. La regla mínima: en producción no se envía el contexto completo, se envían los tipos de evento y los valores de estado, que son cadenas de dominio sin carga personal. Y el paquete de inspección no debe viajar en el paquete servido: se importa de forma dinámica dentro de una rama condicionada por el entorno, para que el empaquetador la elimine al construir.

El comportamiento observable es una consecuencia de haberlo nombrado

Hay una razón por la que ningún panel de depuración de un montón de banderas booleanas ha llegado nunca a existir, y no es que nadie lo haya intentado. Es que no hay nada que mostrar. Una traza necesita eventos con nombre, estados con nombre y transiciones que sean acontecimientos discretos con un antes y un después; un conjunto de variables mutables sueltas no tiene ninguna de las tres cosas, porque una asignación no es un acontecimiento del que se pueda decir cuándo empezó ni qué lo causó. Al obligarte a nombrar los estados y a declarar los eventos, la máquina no solo ordena tu lógica: fabrica el vocabulario mínimo sin el cual la observabilidad es literalmente imposible de construir. Y esto invierte una creencia muy extendida en nuestra profesión, la de que la observabilidad es algo que se añade a un sistema cuando el sistema ya duele —un paquete que se instala, un panel que se compra, unas métricas que se emiten—. La observabilidad no se añade: se hereda de la forma del sistema, y un sistema cuyo comportamiento no está nombrado es opaco de un modo que ninguna herramienta puede reparar desde fuera. La consecuencia práctica va más lejos de lo que parece. Cuando la historia completa de una sesión es un valor pequeño y serializable, el informe de error deja de ser una narración humana ambigua —lo intenté dos veces y se quedó colgado— y pasa a ser un dato que reproduce el fallo en tu máquina. La distancia entre lo que el usuario vivió y lo que el ingeniero puede ejecutar, que es donde se pierde la mayor parte del tiempo de depuración de nuestra industria, se reduce a cero. No porque hayas comprado una herramienta mejor, sino porque tu programa, al estar obligado a decir qué está haciendo, resulta que sabe contarlo.

⚔️ Convierte la inspección en infraestructura
  1. Instrumenta la máquina más compleja de tu proyecto con createBrowserInspector, con arranque diferido tras pulsar un botón oculto de desarrollo.
  2. Añade un filter que descarte todos los eventos internos con prefijo xstate. y compara la legibilidad de la vista antes y después.
  3. Si tu máquina invoca a otras, localiza en la vista de secuencia un mensaje entre padre e hijo y explica qué transición del hijo lo provocó.
  4. Escribe las funciones de grabación y reproducción de traza y comprueba que reproducir una sesión te deja en el mismo valor de estado que la original.
  5. Sustituye los actores invocados por dobles y fija un reloj simulado; vuelve a reproducir y comprueba que ahora la trayectoria completa coincide, no solo el estado final.
  6. Convierte una prueba de extremo a extremo existente en una afirmación sobre la trayectoria de estados y comprueba que sobrevive a un cambio de maquetación que hoy la rompería.