StackPractices
beginner Por Mathias Paulenko

Plantilla de Triaje de Bugs

Plantilla para clasificar y enrutar reportes de bugs por severidad e impacto.

Temas: devops

Visión General

No todos los bugs son iguales. Un problema cosmético en un interruptor de modo oscuro no es lo mismo que un bug de pérdida de datos en un flujo de pago. Sin un sistema de triaje, los problemas críticos se acumulan en backlogs mientras los ingenieros persiguen ruido de baja prioridad. Esta plantilla crea un sistema de clasificación y enrutamiento repetible para que los bugs correctos lleguen a los equipos correctos con la prioridad adecuada. Combínala con una política de escalado para el lado de “a quién despertamos” y un runbook para el de “qué hacemos una vez enrutado”.

Cuándo Usar

Usa este recurso cuando:

  • Tu backlog de bugs crece más rápido de lo que tu equipo puede cerrar ítems
  • Los problemas críticos de producción están enterrados bajo solicitudes de características menores
  • Múltiples equipos comparten una cola de bugs y la propiedad no está clara

Solución

El pipeline de triaje es corto: cada reporte se clasifica, se enruta y se registra —en ese orden.

flowchart diagram: Reporte entrante
# Triaje de Bugs: `<Proyecto / Producto>`

## 1. Clasificación por Severidad

| Severidad | Impacto | Ejemplos | Tiempo de Respuesta | Objetivo de Resolución |
|-----------|---------|----------|---------------------|------------------------|
| S1 — Crítico | Servicio inutilizable o pérdida de datos | Pago fallando, login roto, corrupción de datos | 15 min | 4 horas |
| S2 — Alto | Característica principal rota; existe workaround | Búsqueda caída, exportación de informes falla | 2 horas | 24 horas |
| S3 — Medio | Característica degradada pero funcional | Páginas lentas, ordenamiento incorrecto | 24 horas | 1 semana |
| S4 — Bajo | Cosmético o inconveniente menor | Botón desalineado, typo, color incorrecto | 1 semana | Próximo sprint |

## 2. Preguntas de Triaje

Responde estas preguntas para cada reporte de bug entrante:

1. **Reproducibilidad**
   - [ ] ¿El bug puede reproducirse consistentemente?
   - [ ] Si es intermitente, ¿cuál es la frecuencia aproximada?
   - [ ] ¿Afecta a un segmento específico de usuarios, dispositivo o navegador?

2. **Impacto**
   - [ ] ¿Bloquea un recorrido principal del usuario (registro, compra, login)?
   - [ ] ¿Afecta a un solo usuario, un subconjunto o todos?
   - [ ] ¿Existe un workaround? ¿Qué tan difícil es?

3. **Regulatorio / Seguridad**
   - [ ] ¿Expone PII o datos sensibles?
   - [ ] ¿Viola un requisito de cumplimiento (PCI, SOC 2, GDPR)?

4. **Recencia**
   - [ ] ¿Apareció este bug después de un release reciente?
   - [ ] ¿Es una regresión de un issue previamente corregido?

## 3. Reglas de Enrutamiento

| Severidad | Asignado | Canal | Escalamiento |
|-----------|----------|-------|--------------|
| S1 | Ingeniero de guardia + Líder de equipo | Página + Sala de guerra | VP de Ingeniería después de 1 hora |
| S2 | Líder de equipo | Slack #incidents | Manager después de 4 horas |
| S3 | Siguiente ingeniero disponible | JIRA / Linear | Líder de equipo si no se resuelve en 3 días |
| S4 | Backlog | JIRA / Linear | Re-evaluar si se acumulan duplicados |

## 4. Registro de Triaje

| Fecha | ID de Bug | Reportado Por | Severidad Inicial | Severidad Final | Responsable | Razón del Cambio |
|-------|-----------|---------------|-------------------|-----------------|-------------|------------------|
| | | | | | | |

## 5. Detección de Duplicados

| Verificación | Método |
|--------------|--------|
| Búsqueda por palabras clave | Buscar en JIRA con mensaje de error / componente |
| Coincidencia de stack trace | Comparar firmas de stack trace |
| Superposición de impacto | Verificar si múltiples reportes referencian el mismo flujo |
| Correlación con release | Filtrar bugs reportados dentro de 48h de un despliegue |

Explicación

La plantilla fuerza una clasificación estructurada antes del enrutamiento. Muchos equipos omiten el triaje y asignan bugs a quien esté disponible, lo que significa que los issues críticos esperan detrás de tickets de baja prioridad. La matriz de severidad usa impacto en el usuario y riesgo para el negocio como ejes principales, no solo “qué tan difícil es arreglarlo”. Un cambio de CSS de una línea que bloquea el checkout es S1; una fuga de memoria compleja que afecta al 0.1% de usuarios es S3. Las reglas de enrutamiento evitan que los bugs de alta severidad se traten como trabajo normal de backlog.

Reportes que no son bugs

Un tercio de la mayoría de colas no es un bug —es mala configuración, comportamiento esperado, duplicados o peticiones de funcionalidad disfrazadas. Dales una salida que no sea la escala de severidad: una resolución “no es bug” con código de motivo obligatorio, y la plantilla de cierre de la sección de revisión. La distinción importa para las métricas —mezclar reportes inválidos en tus cifras de SLA hace el triaje más lento y menos efectivo de lo que es.

Las peticiones de funcionalidad merecen cuidado especial. Los usuarios reportan “X no funciona” cuando quieren decir “X no existe” —para el reportero es la misma experiencia, así que re-etiquetar es parte normal del trabajo. Lo importante es cerrar el círculo: avisa de que su ítem pasó a la cola de producto para que no lo vuelva a reportar el mes siguiente como “sigue roto”.

Variantes

ContextoEnfoque de ClasificaciónEnfoque de Enrutamiento
Aplicación móvilVersión de SO, modelo de dispositivo, reseñas de tiendaCrashlytics agrupa automáticamente por stack trace
API / backendEndpoint, tasa de error, pico de latenciaAlert manager enruta por propietario del servicio
SaaS B2BTamaño de tenant, valor de contrato, SLAÉxito del cliente marca bugs de clientes de alto valor
Juego / consumidorImpacto en monetización, segmento de jugadorEquipo de live ops triaja durante eventos
Bug de seguridadPuntaje CVSS, explotabilidad, exposiciónDirecto al equipo de seguridad; omite la cola estándar

Lo que funciona

  1. Triajea cada bug nuevo dentro de 24 horas de reportado; triaje viejo es triaje fallido
  2. Usa una única fuente de verdad (JIRA, Linear, GitHub Issues) para que los duplicados sean detectables
  3. Requiere un caso de prueba reproducible antes de aceptar S2 o mayor; bugs críticos no confirmados desperdician tiempo de ingeniería
  4. Re-evalúa la severidad si emerge nueva información (por ejemplo, “afecta a todos los usuarios” y no “a algunos”)
  5. Cierra bugs “no se arreglará” explícitamente con una justificación; el silencio crea backlogs de tickets zombie

Errores Comunes

  1. Clasificar bugs por esfuerzo en lugar de impacto (arreglo fácil ≠ alta prioridad)
  2. Dejar que los reportadores establezcan su propia severidad; los usuarios siempre piensan que su bug es crítico
  3. No rastrear decisiones de triaje, llevando a los mismos debates cada semana
  4. Enrutar bugs de seguridad a través de la cola estándar en lugar de directamente a seguridad
  5. Ignorar duplicados; diez reportes del mismo bug parecen diez problemas separados

Severidad en la práctica

La matriz es fácil de aplicar en teoría y difícil en el momento. Algunos ejemplos trabajados para mantener la calibración consistente en el equipo:

ReporteSeveridadPor qué
Checkout devuelve 500 para todos desde el deploy de las 14:00S1Flujo principal roto para todos; regresión de una release
Exportar a CSV pierde filas en silencio en SafariS2Funcionalidad mayor rota, pero hay workaround (Chrome)
El selector de fecha del dashboard arranca en ayerS3Degradado pero funcional; el usuario puede elegir la fecha
El logo se ve 2px descentrado en IE11S4Cosmético, segmento de usuarios menguante
Token de API expuesto en una respuesta de errorS1Override de seguridad —salta la cola normal

Dos reglas prácticas zanjan la mayoría de discusiones. Primera: la severidad sigue el impacto en el usuario, nunca la dificultad del arreglo —un cambio de una línea de CSS que bloquea el checkout es S1 y una fuga de memoria de una semana que afecta al 0,1 % es S3. Segunda: cuando un reporte queda entre dos niveles, asígnale el menor y re-evalúa tras reproducirlo; sobre-clasificar insensibiliza la escala entera.

Implantar el triaje en un equipo

Empieza más pequeño de lo que crees. El primer mes es calibración, no cobertura:

  • Semana 1: nombra el responsable rotatorio de triaje y define los cuatro niveles de severidad con 2-3 ejemplos reales de tu propio backlog —los ejemplos prestados nunca calibran igual.
  • Semanas 2-3: triajea cada bug entrante a diario, registra cada decisión en la tabla del log y compara notas sobre los desacuerdos al final de cada semana.
  • Semana 4: revisa el registro. Los desacuerdos son tus datos de calibración —documenta los casos límite y cómo se resolvieron, e incorpóralos a la plantilla como filas nuevas o ejemplos trabajados.
  • Mes 2+: añade la automatización de abajo (labels, fingerprinting, alertas SLA) cuando el bucle manual sea estable. Automatizar un proceso descalibrado solo produce enrutado erróneo con confianza y a escala.

Mantén el log aunque parezca burocracia —es el único artefacto que convierte “todos sabemos qué es crítico” en algo que un recién llegado puede auditar.

Métricas de triaje que importan

Sigue cuatro números mensualmente; lo demás es vanidad:

  • Tiempo hasta triaje: horas medias desde el reporte hasta severidad + responsable. Objetivo < 24 h.
  • Tasa de duplicados: % de reportes cerrados como duplicados. Tasas altas indican que la deduplicación no corre pronto.
  • Tasa de ajuste de severidad: % de bugs cuya severidad inicial cambió tras reproducirlos. Números altos sostenidos significan que las preguntas de la sección 2 no se responden.
  • Cumplimiento de SLA: % de bugs resueltos dentro de su objetivo de severidad, según la tabla de enrutado. Desglosado por severidad —un 90 % agregado puede esconder un 50 % en S1.

Una semana de triaje, ejemplo trabajado

Las reglas abstractas se fijan cuando ves una cola procesada. Lunes por la mañana, siete reportes esperan:

  • “La página de pago devuelve pantalla en blanco” —sin pasos ni navegador. Antes de clasificar, pides el entorno; el reportero confirma que falla en todos los dispositivos. Lo reproduces, ves un bundle JS roto tras el deploy de anoche y lo marcas S1 —pagina al on-call y aterriza en el war room de la release.
  • “Los resultados de búsqueda se ven desactualizados” —reproducible, afecta a un plan, y un refresh lo arregla. S3, enrutado al equipo de búsqueda; el responsable pide la query exacta y encuentra un TTL de caché mal configurado dos horas después.
  • Tres reportes de “el botón exportar no hace nada” —síntomas idénticos de clientes distintos. La deduplicación los detecta como un solo bug; S2 al servicio de exportación. El número de duplicados, no un reportero concreto, es lo que lo sube de S3.
  • “El icono de modo oscuro tiene un tooltip equivocado” —cosmético, S4, directo al backlog.
  • “La API devuelve datos de mi cliente al pasar un token inválido” —mal en todos los niveles de la escala normal, porque pertenece a la cola de seguridad. Enrutado fuera de banda con el reloj S1 corriendo bajo confidencialidad.

El jueves, el log muestra nueve decisiones —cinco mantuvieron su severidad inicial, tres se ajustaron tras reproducir, una se cerró como duplicado. Ese registro es lo que permite al revisor del viernes auditar la semana sin re-litigar cada llamada: cada fila dice qué se decidió, quién lo decidió y por qué cambió cuando cambió.

A los dos meses, el patrón a vigilar es qué ajustes se repiten. Si los reportes “intermitentes” empiezan siempre en S3 y suben a S2, tus preguntas de reproducción no capturan bien la frecuencia —arregla las preguntas de la plantilla, no a la gente que las aplica.

Soluciones Avanzadas

Triaje automatizado de bugs con GitHub Issues y labels

Usa GitHub Actions para auto-etiquetar y enrutar reportes de bugs entrantes basado en análisis de contenido:

# .github/workflows/auto-triage.yml
name: Auto-Triage Bug Reports
on:
  issues:
    types: [opened, labeled]

jobs:
  triage:
    runs-on: ubuntu-latest
    steps:
      - name: Check for severity keywords
        uses: actions/github-script@v7
        with:
          script: |
            const body = context.payload.issue.body || "";
            const title = context.payload.issue.title || "";
            const text = (title + " " + body).toLowerCase();

            const severityRules = [
              { label: "s1-critical", keywords: ["data loss", "payment", "login broken", "security", "production down"] },
              { label: "s2-high", keywords: ["broken", "crash", "error 500", "not working", "failing"] },
              { label: "s3-medium", keywords: ["slow", "incorrect", "wrong", "unexpected"] },
              { label: "s4-low", keywords: ["typo", "cosmetic", "alignment", "color"] },
            ];

            for (const rule of severityRules) {
              if (rule.keywords.some(kw => text.includes(kw))) {
                await github.rest.issues.addLabels({
                  owner: context.repo.owner,
                  repo: context.repo.repo,
                  issue_number: context.payload.issue.number,
                  labels: [rule.label, "needs-triage"],
                });
                break;
              }
            }

Desduplicación de bugs con fingerprinting de stack traces

Agrupa reportes de crashes similares normalizando y hasheando stack traces:

import hashlib
import re
from collections import defaultdict
from typing import List, Dict

def normalize_stack_trace(trace: str) -> str:
    """Normalize a stack trace by removing line numbers and memory addresses."""
    # Remove file paths, keeping only filenames
    trace = re.sub(r'/[\w/.-]+/', '', trace)
    # Remove line numbers
    trace = re.sub(r':\d+', '', trace)
    # Remove memory addresses
    trace = re.sub(r'0x[0-9a-fA-F]+', '0xADDR', trace)
    # Remove thread IDs
    trace = re.sub(r'Thread-\d+', 'Thread-N', trace)
    return trace.strip()

def fingerprint_stack_trace(trace: str) -> str:
    """Generate a hash fingerprint from a normalized stack trace."""
    normalized = normalize_stack_trace(trace)
    return hashlib.sha256(normalized.encode()).hexdigest()[:16]

def group_bug_reports(reports: List[Dict]) -> Dict[str, List[Dict]]:
    """Group bug reports by stack trace fingerprint."""
    groups = defaultdict(list)
    for report in reports:
        trace = report.get("stack_trace", "")
        if trace:
            fp = fingerprint_stack_trace(trace)
            groups[fp].append(report)
        else:
            groups["no-trace"].append(report)
    return groups

# Example usage
bug_reports = [
    {"id": "BUG-001", "stack_trace": "TypeError at /app/src/handlers.py:42\n  File /app/src/utils.py:128"},
    {"id": "BUG-002", "stack_trace": "TypeError at /app/src/handlers.py:45\n  File /app/src/utils.py:131"},
    {"id": "BUG-003", "stack_trace": "ValueError at /app/src/models.py:67"},
]

groups = group_bug_reports(bug_reports)
for fp, reports in groups.items():
    print(f"Fingerprint {fp}: {len(reports)} reports - {[r['id'] for r in reports]}")

Dashboard de envejecimiento de bugs con notificación Slack

Rastrea bugs que exceden su objetivo de resolución SLA y alerta al equipo:

import os
from datetime import datetime, timedelta
from dataclasses import dataclass
from typing import List

SLA_TARGETS = {
    "S1": timedelta(hours=4),
    "S2": timedelta(hours=24),
    "S3": timedelta(days=7),
    "S4": timedelta(days=14),
}

@dataclass
class BugTicket:
    id: str
    severity: str
    created_at: datetime
    status: str
    assignee: str

def find_aged_bugs(tickets: List[BugTicket]) -> List[BugTicket]:
    """Find bugs that have exceeded their SLA resolution target."""
    now = datetime.now()
    aged = []
    for ticket in tickets:
        if ticket.status in ("closed", "resolved"):
            continue
        sla = SLA_TARGETS.get(ticket.severity)
        if not sla:
            continue
        age = now - ticket.created_at
        if age > sla:
            aged.append(ticket)
    return aged

def format_slack_alert(aged_bugs: List[BugTicket]) -> str:
    """Format aged bugs for a Slack alert message."""
    if not aged_bugs:
        return "No bugs have exceeded their SLA target."

    lines = [":warning: *SLA Violation Alert* — Bugs exceeding resolution target:\n"]
    for bug in sorted(aged_bugs, key=lambda b: b.created_at):
        age_hours = (datetime.now() - bug.created_at).total_seconds() / 3600
        sla_hours = SLA_TARGETS[bug.severity].total_seconds() / 3600
        lines.append(
            f"• {bug.severity} {bug.id} — {age_hours:.0f}h old "
            f"(SLA: {sla_hours:.0f}h) — Assigned: {bug.assignee}"
        )
    return "\n".join(lines)

# Example usage
tickets = [
    BugTicket("BUG-100", "S1", datetime.now() - timedelta(hours=6), "open", "alice"),
    BugTicket("BUG-101", "S2", datetime.now() - timedelta(hours=30), "open", "bob"),
    BugTicket("BUG-102", "S3", datetime.now() - timedelta(days=2), "open", "charlie"),
]

aged = find_aged_bugs(tickets)
print(format_slack_alert(aged))

Revisión de bugs obsoletos

Los bugs de baja severidad se acumulan rápido y se vuelven irrelevantes sin ruido. Dos hábitos mantienen el backlog honesto:

  1. Un “bug bash” mensual —una hora en la que el equipo revisa tickets S3/S4 abiertos de más de 90 días y los reproduce, reprioriza o cierra con motivo explícito:
#!/bin/bash
# Find S3/S4 bugs older than 90 days with no activity
gh issue list \
  --label "s3-medium,s4-low" \
  --state open \
  --search "created:<$(date -d '90 days ago' +%Y-%m-%d) updated:<$(date -d '30 days ago' +%Y-%m-%d)" \
  --json number,title,createdAt,updatedAt \
  --jq '.[] | "#\(.number) \(.title) (created: \(.createdAt[:10]))"'
  1. Una plantilla de cierre fija para reportes que resultan no ser bugs —una redacción consistente mantiene los cierres educados y buscables:
Gracias por reportar este problema. Tras investigarlo, parece ser:
- [ ] Un error de configuración por parte del usuario
- [ ] Comportamiento esperado, no un bug
- [ ] Un duplicado de #{existing_issue}
- [ ] Una petición de funcionalidad, no un bug

Se cierra como: {reason}. Si crees que es incorrecto, reábrelo con contexto adicional.

Preguntas frecuentes

¿Qué pasa si un reporte de bug es vago o le faltan pasos de reproducción?

Solicita la información estándar: pasos para reproducir, comportamiento esperado vs real, entorno (navegador, SO, versión), capturas de pantalla o grabaciones, y logs de error. Si el reportador no puede proporcionar esto dentro de 48 horas, reduce a S4 o cierra como "necesita info". No dejes que reportes incompletos bloqueen el triaje de bugs útiles.

¿Cómo evito que el triaje se convierta en un cuello de botella?

Rota un ingeniero de "deber de triaje" cada semana. Esta persona revisa todos los bugs entrantes durante 30 minutos cada mañana, clasifica, enruta y solicita información faltante. El deber de triaje no debe ser la misma persona que el de guardia. Con el tiempo, automatiza: usa reportes de crashes para pre-clasificar por stack trace, y reglas de bots para auto-enrutar a propietarios conocidos de componentes.

¿Cuándo debería re-evaluarse la severidad?

Siempre que la información nueva cambie la evaluación de impacto —un reporte de "algunos usuarios" que resulta afectar a todos, un workaround que deja de funcionar, o un bug cosmético que empieza a generar duplicados. La columna "Motivo del cambio" del registro existe justo para esto: la severidad es una hipótesis de trabajo, no un veredicto.

¿Pasan los bugs de seguridad por esta plantilla?

No —al menos no por la cola estándar. Cualquier reporte con implicaciones de seguridad o exposición de datos va directo al canal del equipo de seguridad con sus propias reglas de confidencialidad. El S1 de la plantilla sigue aplicando para la velocidad de respuesta, pero la visibilidad queda restringida hasta que el parche salga.

¿Deben las solicitudes de características ser triajeadas junto con bugs?

No. Mantén bugs y solicitudes de características en colas separadas con objetivos de SLA distintos. Las solicitudes de características requieren input de producto; los bugs requieren input de ingeniería. Mezclarlos genera confusión sobre propiedad y prioridad. Si un usuario reporta un bug que en realidad es una característica faltante, re-etiquétalo y muévelo al backlog de producto con una explicación clara.