Breakpoints condicionales y logpoints: instrumentar sin tocar el código
Detenerse solo cuando importa, registrar valores sin modificar ficheros, y el truco de la condición con efecto lateral que convierte un breakpoint en cualquier cosa.
Un breakpoint que se detiene mil veces no es una herramienta, es un obstáculo. Un breakpoint condicional se detiene solo cuando la expresión que escribes es verdadera, y con eso el bucle de mil iteraciones pasa a detenerse en la única que importa. Un logpoint da un paso más: no detiene nada, escribe en la consola exactamente lo que quieras, sin tocar el fichero, sin recompilar y sin riesgo de que se quede en producción. Entre los dos cubren casi todo lo que la gente hace con console.log, mejor y más rápido.
- Escribir condiciones eficaces para detenerse solo en el caso interesante.
- Sustituir las trazas de investigación por logpoints y enumerar las ventajas.
- Aplicar el truco de la condición con efecto lateral para contar, trazar y medir.
- Reconocer los tres errores que hacen que un breakpoint condicional no funcione.
El breakpoint condicional
Botón derecho en el número de línea, opción de añadir breakpoint condicional, y un campo donde escribir una expresión. El depurador la evalúa en el contexto de esa línea cada vez que la ejecución pasa por ahí, y se detiene solo si el resultado es verdadero.
La expresión tiene acceso a todo lo que esté en el ámbito en ese punto: parámetros, variables locales, closures, this, y el global.
// En un bucle de mil pedidos, parar solo en el problematico
pedido.id === 4821
// Parar solo cuando el valor es invalido
!Number.isFinite(total)
// Parar solo a partir de cierta iteracion
i > 500
// Parar solo si el usuario tiene un rol concreto
usuario?.roles?.includes('admin')
// Parar solo si la pila viene de donde sospechas
new Error().stack.includes('procesarCola')
Ese último merece atención: permite condicionar por procedencia, que es lo que necesitas cuando una función se llama desde diez sitios y solo uno da problemas.
La condición se evalúa en cada paso, así que una condición cara ejecutada un millón de veces ralentiza notablemente. En bucles muy calientes conviene escribir la condición barata primero y aprovechar el cortocircuito: i > 9000 && calcularAlgoCaro() evalúa lo caro solo nueve mil veces menos.
Los logpoints
La misma opción del menú contextual ofrece un punto de registro. En vez de detenerse, evalúa una expresión y escribe el resultado en la consola. La ejecución continúa sin interrupción.
En el campo se escribe lo que quieras que salga, y admite varios valores separados por comas igual que console.log.
'pedido', pedido.id, 'total', total, 'usuario', usuario?.nombre
Las cinco ventajas sobre un console.log escrito en el fichero, y todas son sustanciales.
No modifica el código. No hay que recompilar, no hay que recargar, no hay riesgo de dejarlo en un commit.
Se pone y se quita al instante. Un clic. La instrumentación deja de tener coste de ciclo.
Funciona en código que no puedes editar. Una librería de node_modules, un script de un proveedor, código de producción. Ahí es donde no hay alternativa.
Se gestiona en bloque. Todos los logpoints están en la lista de breakpoints y se desactivan de golpe.
Puede mostrar la pila. Escribiendo la expresión adecuada, el logpoint da lo mismo que un console.trace sin tocar nada.
La única pega real es que los logpoints no se comparten con el equipo: son de tu perfil. Para instrumentación que debe verla otra persona, el código sigue siendo el sitio.
El truco de la condición con efecto lateral
Aquí está la técnica que convierte un breakpoint condicional en una herramienta de propósito general. La condición se evalúa como JavaScript arbitrario, así que puede hacer cosas, y si al final devuelve algo falso, la ejecución no se detiene.
// Un contador sin detenerse nunca
console.count('paso por aqui') && false
console.count devuelve undefined, que es falso, así que la condición nunca se cumple y el breakpoint nunca detiene. Pero el contador sube. Es un contador instalado sin tocar el código.
Las variantes útiles.
// Traza de pila cada vez que pasa, sin detener
console.trace('llamada') && false
// Acumular valores para analizarlos despues
(globalThis.__muestras ??= []).push({ id: pedido.id, total }) && false
// Medir cuanto tarda entre dos puntos, poniendo esto en el primero
(globalThis.__t0 = performance.now()) && false
// Y esto en el segundo
(console.log('ms', (performance.now() - globalThis.__t0).toFixed(2))) && false
// Detener solo la primera vez
(globalThis.__yaPare ??= false) ? false : (globalThis.__yaPare = true)
// Detener a partir de la decima vez
(globalThis.__n = (globalThis.__n ?? 0) + 1) > 10
El patrón de acumular en un array global es especialmente potente: dejas el breakpoint puesto, usas la aplicación con normalidad durante un minuto, y luego vuelcas los datos.
console.table(globalThis.__muestras);
Eso da una muestra de cientos de ejecuciones reales sin haber detenido la aplicación ni una vez y sin haber escrito una sola línea en ningún fichero.
Los tres errores que hacen que no funcione
Escribir una asignación en vez de una comparación. pedido.id = 4821 asigna, devuelve el valor asignado —que es verdadero— y además corrompe el dato. El breakpoint se detiene siempre y encima has cambiado el programa. Es el error más caro de los tres porque además del ruido, introduce un bug nuevo.
Referirse a una variable que aún no está en el ámbito. Si pones la condición en la primera línea de una función y usas una variable declarada más abajo, la evaluación lanza un error. Cuando la condición falla al evaluarse, el comportamiento es detenerse, con lo que parece que la condición se cumple siempre. Si un condicional se detiene sin motivo, comprueba que la expresión sea válida en ese punto.
Poner la condición en la línea equivocada. Una condición sobre un parámetro puesta antes de la línea que lo declara, o sobre una variable de bucle puesta fuera del bucle. El ámbito de la expresión es exactamente el de esa posición.
Hay una consecuencia de los logpoints y de las condiciones con efecto lateral que va mucho más allá de la comodidad, y es la que justifica dedicarles una lección entera: eliminan el coste marginal de una hipótesis. Cuando instrumentar cuesta un ciclo de edición, guardado, compilación y recarga, cada hipótesis que quieres comprobar cuesta entre uno y tres minutos, y el resultado es que solo compruebas las hipótesis en las que ya crees bastante. Las corazonadas débiles se descartan sin probarlas, y las corazonadas débiles son precisamente donde suelen estar los bugs raros, porque los fáciles ya los habrías encontrado. Cuando instrumentar cuesta dos clics y no requiere recargar, el cálculo cambia por completo: puedes poner ocho logpoints en ocho sitios distintos de un flujo, ejecutarlo una vez, y leer la secuencia completa de lo que pasó y en qué orden. Eso no es una versión más rápida de lo mismo: es una técnica distinta, porque una sola reproducción del bug te da toda la información en vez de un dato, y en bugs difíciles de reproducir esa diferencia lo es todo. El caso extremo que ilustra el cambio: un fallo que ocurre una vez de cada treinta intentos. Con trazas, cada intento fallido te da un dato y hay que repetir treinta veces por cada cosa que quieras saber. Con ocho logpoints y un breakpoint condicional que se detiene solo cuando la invariante se rompe, el primer fallo que ocurra te deja la aplicación congelada en el instante exacto, con la secuencia completa de lo que pasó antes ya escrita en la consola, y con todo el estado disponible para inspeccionar. La diferencia entre las dos formas de trabajar no se mide en minutos: se mide en si el bug se resuelve o se queda sin resolver.