All MicroEvals
JVG Agentic Coding — Debugging & Root Cause v0.1
Create MicroEval

JVG Agentic Coding — Debugging & Root Cause v0.1

Prompt

Actúa como agente de desarrollo senior depurando un fallo real en producción. No estás diseñando un sistema desde cero. Tu responsabilidad es separar síntomas de causas, construir una explicación causal apoyada en la evidencia disponible y aplicar el cambio mínimo que elimine la causa raíz sin introducir regresiones. ## Contexto Aplicación construida con: - Next.js con App Router - React - TypeScript - PostgreSQL - sin ORM relevante para este ejercicio - sin librerías adicionales que debas instalar La aplicación permite crear una nueva sesión para un paciente. En producción aparece un problema intermitente: algunas veces una única acción del usuario termina creando dos sesiones casi idénticas. El equipo sospechó inicialmente: - doble clic; - React Strict Mode; - que el botón tarda demasiado en deshabilitarse. Sin embargo, los logs del cliente muestran un único `handleCreate` para los casos afectados. ## Código relevante ### components/sessions/CreateSessionButton.tsx ```tsx "use client"; import { useRouter } from "next/navigation"; import { useRef, useState } from "react"; import { apiFetch } from "@/lib/apiFetch"; type Session = { id: string; patientId: string; }; export function CreateSessionButton({ patientId, }: { patientId: string; }) { const router = useRouter(); const inFlightRef = useRef(false); const [pending, setPending] = useState(false); async function handleCreate() { if (inFlightRef.current) return; inFlightRef.current = true; setPending(true); const clientRequestId = crypto.randomUUID(); try { const session = await apiFetch<Session>("/api/sessions", { method: "POST", headers: { "content-type": "application/json", }, body: JSON.stringify({ patientId, clientRequestId, }), }); router.push(`/sessions/${session.id}`); } finally { inFlightRef.current = false; setPending(false); } } return ( <button type="button" disabled={pending} onClick={handleCreate}> {pending ? "Creando…" : "Nueva sesión"} </button> ); } ``` ### lib/apiFetch.ts ```ts export async function apiFetch<T>( input: RequestInfo | URL, init?: RequestInit, ): Promise<T> { for (let attempt = 0; attempt < 2; attempt++) { let response: Response; try { response = await fetch(input, init); } catch (error) { if (attempt === 0) { continue; } throw error; } if (response.status >= 500 && attempt === 0) { continue; } if (!response.ok) { throw new Error(`HTTP ${response.status}`); } return (await response.json()) as T; } throw new Error("Request failed after retry"); } ``` El retry automático se introdujo porque parte de los usuarios trabaja con conexiones inestables. Debe conservarse. El mismo `RequestInit` se reutiliza durante el retry, por lo que el segundo intento contiene el mismo body y el mismo `clientRequestId`. ### app/api/sessions/route.ts ```ts import { NextResponse } from "next/server"; import { analytics } from "@/lib/analytics"; import { db } from "@/lib/db"; export async function POST(request: Request) { const body = (await request.json()) as { patientId?: string; clientRequestId?: string; }; if (!body.patientId || !body.clientRequestId) { return NextResponse.json( { error: "invalid_request" }, { status: 400 }, ); } const session = await db.session.create({ patientId: body.patientId, }); await analytics.track("session_created", { sessionId: session.id, patientId: session.patientId, }); return NextResponse.json(session, { status: 201 }); } ``` ### lib/db.ts No debes reescribir este archivo. Para este ejercicio puedes asumir que estas funciones ya existen y funcionan con las firmas mostradas: ```ts type Session = { id: string; patientId: string; clientRequestId: string | null; }; type CreateSessionInput = { patientId: string; clientRequestId?: string; }; export const db = { session: { create(input: CreateSessionInput): Promise<Session>, findByClientRequestId( clientRequestId: string, ): Promise<Session | null>, }, }; export function isUniqueViolation( error: unknown, constraint: string, ): boolean; ``` Cuando `clientRequestId` se omite de `db.session.create`, la fila se persiste con `client_request_id = NULL`. Cuando se proporciona, se persiste en esa columna. `isUniqueViolation(error, "sessions_client_request_id_key")` devuelve `true` exclusivamente cuando PostgreSQL rechaza la escritura por esa constraint concreta. ### Esquema relevante de PostgreSQL ```sql CREATE TABLE sessions ( id uuid PRIMARY KEY, patient_id uuid NOT NULL, client_request_id text UNIQUE, created_at timestamptz NOT NULL DEFAULT now() ); ``` No hace falta una migración: `client_request_id` y su constraint única ya existen. ### lib/analytics.ts ```ts export const analytics = { async track( event: string, properties: Record<string, string>, ): Promise<void> { // Servicio externo. // Puede lanzar una excepción temporal si el proveedor no responde. }, }; ``` Analytics es best-effort. Un fallo de analytics no debe convertir una sesión ya creada correctamente en una operación fallida para el usuario. ## Evidencia de un incidente real Cliente: ```text 10:14:03.102 handleCreate patient=p-17 clientRequestId=req-8f2 ``` Servidor: ```text 10:14:03.180 POST /api/sessions 10:14:03.214 session created id=s-101 patient=p-17 client_request_id=NULL 10:14:03.301 analytics session_created -> 503 10:14:03.304 POST /api/sessions -> 500 10:14:03.420 POST /api/sessions 10:14:03.458 session created id=s-102 patient=p-17 client_request_id=NULL 10:14:03.520 analytics session_created -> 200 10:14:03.523 POST /api/sessions -> 201 ``` Las dos peticiones pertenecen a la misma llamada de `apiFetch`. En otros incidentes el primer POST sí termina correctamente en servidor, pero la conexión se corta antes de que el cliente reciba la respuesta. El retry puede entonces volver a entregar exactamente el mismo body. ## Comportamiento requerido Una acción lógica de creación debe producir como máximo una sesión. En concreto: 1. El mismo `clientRequestId` puede llegar al servidor más de una vez. 2. Todas esas entregas deben resolver a la misma sesión persistida. 3. Dos `clientRequestId` distintos sí pueden crear dos sesiones diferentes. 4. El retry automático de `apiFetch` debe conservarse. 5. Un fallo de analytics no debe provocar que una sesión ya persistida se considere fallida. 6. Una pérdida de la respuesta después de haber persistido la sesión tampoco debe permitir crear una segunda sesión en el retry. 7. Debe seguir siendo seguro si dos peticiones con el mismo `clientRequestId` llegan casi simultáneamente. ## Restricciones Debes respetarlas todas. 1. No añadas dependencias. 2. No elimines ni desactives globalmente el retry de `apiFetch`. 3. No añadas una migración ni cambies el esquema: la constraint necesaria ya existe. 4. No resuelvas el problema únicamente con debounce, `disabled`, `useRef`, Strict Mode o lógica equivalente del cliente. 5. No cambies rutas, autenticación ni contratos no relacionados. 6. No reescribas `lib/db.ts`. 7. Usa únicamente las funciones de `db` e `isUniqueViolation` cuya interfaz se muestra arriba. 8. No inventes una cola, lock distribuido, servicio externo o infraestructura nueva. 9. No introduzcas una transacción si no es necesaria para preservar las propiedades requeridas. 10. No conviertas analytics en requisito para confirmar la creación. 11. Evita capturar como duplicado un error de base de datos distinto a la constraint `sessions_client_request_id_key`. 12. Preserva el comportamiento funcional existente salvo donde sea necesario corregir el bug. 13. No uses pseudocódigo. 14. No afirmes haber ejecutado tests o reproducido el fallo si no puedes hacerlo. 15. Si una conclusión depende de una suposición no demostrable con la información proporcionada, indícala. ## Qué debes hacer Antes de modificar código: - reconstruye la cadena causal completa del incidente; - distingue la causa raíz de factores que solo hacen el fallo más visible; - explica por qué las soluciones superficiales propuestas por el equipo no garantizan la propiedad requerida. Después implementa la corrección mínima. Presta especial atención a: - idempotencia; - retries de operaciones no idempotentes; - fallo después de un efecto durable; - concurrencia entre dos entregas equivalentes; - constraint única existente; - semántica de analytics; - diferencia entre «la petición falló» y «no sabemos si el efecto ocurrió». ## Formato obligatorio de respuesta Devuelve exactamente estas cinco secciones y en este orden: ## 1. Diagnóstico causal Máximo 8 puntos. Incluye la secuencia causal del incidente y separa causa raíz de síntomas o amplificadores. ## 2. Plan de corrección Indica únicamente los archivos que modificarías y por qué. No incluyas archivos que no necesiten cambios. ## 3. Implementación Proporciona el contenido final completo de cada archivo modificado. Cada archivo debe comenzar con: ### ruta/del/archivo No uses pseudocódigo. ## 4. Verificación Explica cómo verificarías al menos: - analytics devuelve 503 después de persistir; - se pierde la respuesta después de persistir; - llegan dos POST concurrentes con el mismo `clientRequestId`; - llegan dos POST con distintos `clientRequestId`; - un error de base de datos distinto a la constraint de idempotencia; - regresión del flujo normal. Distingue claramente lo demostrable por razonamiento del código de lo que requeriría ejecutar realmente la aplicación o tests. ## 5. Riesgos o supuestos Incluye únicamente riesgos o supuestos materiales. Si no existen, escribe: Ninguno material.

Drag to resize
Drag to resize
Drag to resize
Drag to resize