Skip to content

Publicado, sano y roto: el WhatsApp Flow que Meta aceptó y ningún teléfono podía abrir

· 7 min de lectura

tags: architecture, recruiting, dev-tools

En Selenios, un candidato puede postularse a un puesto sin salir de WhatsApp. Le escribe al número de la empresa, acepta el aviso de privacidad y recibe un formulario nativo dentro del chat: un WhatsApp Flow. Lo completa y toca enviar. Generamos un Flow por vacante a partir del formulario de postulación que armó el recruiter en nuestra plataforma, lo publicamos con la API de Meta y lo cacheamos hasta que el formulario cambia.

La semana pasada un cliente nos mandó una grabación de pantalla. En una vacante, cada candidato que tocaba "Completar" veía un spinner y después una hoja gris que decía "Se produjo un error. Vuelve a intentarlo más tarde." Otras vacantes del mismo cliente, en el mismo número, funcionaban.

La API de Meta decía que el Flow estaba publicado, sin errores de validación y con los chequeos de salud en verde. Igual estaba roto para todo el que lo abría. Esta nota cuenta cómo encontramos la causa y qué armamos para que el próximo problema de este tipo nos llegue a nosotros antes que la grabación de un cliente.

Nada de nuestro lado

Lo primero fueron nuestros logs. En el minuto de la grabación, la conversación se veía normal. El webhook recibió el mensaje del candidato, convertimos el formulario de la vacante en once campos, encontramos un Flow ya publicado para ese formulario exacto y lo enviamos. Ni errores, ni advertencias, nada en Sentry.

Ese silencio era esperable. Nuestros Flows son estáticos: el mensaje abre la primera pantalla con flow_action: "navigate", y todas las pantallas están definidas en el propio Flow JSON. No hay data endpoint, así que abrir el formulario no le hace ningún pedido a nuestro backend. Si el Flow se rompe al abrirse, se rompe en el teléfono, y a nosotros no nos llega nada.

Todo en verde del lado de Meta

El siguiente sospechoso era el Flow, así que se lo pregunté a Meta (pasamos por Kapso, que hace de proxy de la Graph API):

GET /{flow-id}?fields=status,validation_errors,health_status

{
  "status": "PUBLISHED",
  "validation_errors": [],
  "json_version": "7.0",
  "health_status": {
    "can_send_message": "AVAILABLE",
    "entities": [
      { "entity_type": "FLOW",     "can_send_message": "AVAILABLE" },
      { "entity_type": "WABA",     "can_send_message": "AVAILABLE" },
      { "entity_type": "BUSINESS", "can_send_message": "AVAILABLE" },
      { "entity_type": "APP",      "can_send_message": "AVAILABLE" }
    ]
  }
}

Publicado, sin errores de validación y con todas las entidades habilitadas para enviar. Un Flow de una vacante que funcionaba devolvía lo mismo. Según esta evidencia, el Flow roto estaba sano.

Es lo primero que conviene saber de los WhatsApp Flows. health_status responde ¿puede este negocio enviar este Flow?: el estado de la cuenta, del número, de la app. No dice si el Flow se puede mostrar. validation_errors es lo que haya detectado el validador al publicar y, como se vio después, ese validador no revisa todo lo que revisa el teléfono.

Leer el JSON que Meta sirve de verdad

Meta permite descargar el Flow JSON publicado: GET /{flow-id}/assets devuelve una download_url firmada. Descargué el Flow roto y uno que funcionaba, y los comparé componente por componente.

La diferencia estaba en un Dropdown, el que pregunta dónde vive el candidato. Abreviado:

{
  "type": "Dropdown",
  "name": "lugar_de_residencia",
  "data-source": [
    { "id": "Norte",  "title": "Norte" },
    { "id": "Centro", "title": "Centro" },
    { "id": "Sur",    "title": "Sur" },
    { "id": "Norte",  "title": "Norte (4)" },
    { "id": "Centro", "title": "Centro (5)" },
    { "id": "Sur",    "title": "Sur (6)" },
    ...
  ]
}

(Cambié los nombres de las sucursales. La lista real tenía 24 opciones.) Tres aparecían dos veces en el formulario del recruiter, probablemente por pegar una lista encima de otra que ya estaba. En el formulario web una opción repetida es cosmética: el candidato ve la misma sucursal dos veces y elige cualquiera. Nuestro generador de Flows ya tenía una regla para opciones largas que, al recortarse, quedan con el mismo título visible: les agrega un contador para que el candidato las distinga. Esa regla cambiaba los títulos y dejaba los ids como estaban, porque el id es el valor contra el que validamos la respuesta. Así que el Dropdown salió con tres ids repetidos.

El validador de Meta lo aceptó y lo publicó. El cliente de WhatsApp lo rechazó apenas intentó mostrar la pantalla.

Al principio era solo una inferencia: los ids repetidos eran la única anomalía entre el Flow roto y los que funcionaban, y ningún otro Flow publicado nuestro los tenía. Como mandamos todo a través de Kapso, también quería descartar al proxy. Dos chequeos de solo lectura resolvieron las dos dudas. Primero, el Flow no tiene endpoint_uri, así que al abrirlo no se llama ni a Kapso ni a nosotros: el teléfono descarga el JSON desde el CDN de Meta y lo muestra. Segundo, la Graph API devuelve una URL de preview que muestra el Flow dentro del propio Business Manager de Meta. Ahí el Flow que funcionaba abrió normal. El roto abrió con esto:

Component Validation Error
Dropdown 'lugar_de_residencia' has duplicate ID's.

O sea, el renderer de Meta conoce la regla y tiene un mensaje preciso para ella. El validador que respondió PUBLISHED con validation_errors: [] no la aplica. Y el candidato en el teléfono nunca vio el mensaje preciso, solo "Se produjo un error".

El arreglo, y por qué importa subir un número de versión

El arreglo en el generador es una línea: descartar las opciones repetidas y quedarse con la primera. Las dos copias tienen el mismo valor, así que no se pierde nada.

Esa línea sola no habría arreglado la vacante rota. Cacheamos cada Flow publicado bajo un hash de lo que lo genera: los campos, las etiquetas, el idioma y una constante GENERATOR_VERSION. El formulario del recruiter no había cambiado, así que el hash tampoco, y el Flow roto se habría seguido enviando para siempre. Subir la versión del generador cambia todos los hashes, así que cada Flow cacheado se republica una vez, en su próxima conversación. La vacante se arregla con el deploy sin que nadie toque su formulario.

Después cerramos la puerta:

  • El editor de formularios marca la opción repetida y no deja guardar hasta que se elimine, y al pegar una lista saltea las opciones que ya existen.
  • El backend rechaza un formulario con una opción repetida, sin distinguir espacios ni mayúsculas, con un mensaje que nombra la opción. Antes de activarlo contamos cuántos formularios guardados en producción rechazaría: uno. No queda ningún dato viejo bloqueado.

Nuestros chequeos, antes que los de Meta

Lo que más me importa es el monitoreo. El problema de fondo es general: el validador de Meta al publicar y el cliente de WhatsApp no aplican las mismas reglas. Un Flow puede estar PUBLISHED y no servir, y con un Flow estático la falla ocurre en el teléfono, donde no la vemos.

Así que ahora cada publicación pasa por nuestro propio lint sobre el JSON generado antes de ir a Meta. Busca problemas que un validador deja pasar y que un renderer puede rechazar: un id repetido en cualquier data-source, un nombre de componente repetido dentro de una pantalla. No bloquea la publicación, la reporta. Cada hallazgo es una advertencia en Sentry con el cliente y la vacante, y un formulario con opciones repetidas en su origen tiene su propia advertencia que nombra los campos, aunque el generador ya las limpie. Las dos corren una vez por publicación, no una vez por conversación, así que un formulario mal armado aparece una vez y no una por candidato.

La lista de chequeos hoy es corta. Va a crecer a medida que aprendamos qué más pasa el validador y falla en el teléfono.

Lo que me llevo

  • "Publicado" quiere decir que el validador lo aceptó, no que funciona. Un estado verde sobre contenido generado es una señal, no una prueba.
  • Hay que saber qué cubre un health check. can_send_message habla del estado de la cuenta. No dice nada del contenido que enviás.
  • Estático quiere decir sin telemetría. Si al abrir la pantalla no le llega nada a tu servidor, de las fallas te enterás por los usuarios. O validás más antes de publicar, o agregás un data endpoint que te dé una señal.
  • Leé el artefacto que se sirve de verdad. Nuestros logs, nuestra base y el endpoint de estado de Meta parecían correctos. El bug solo se veía en el JSON que sirve Meta, a dos llamadas de API.
  • Versioná el generador. Si cacheás contenido generado bajo un hash, un arreglo en el generador no llega a lo que ya está cacheado salvo que la versión forme parte del hash.

Y algo que le pediría a Meta: que el validador al publicar revise lo mismo que revisa el cliente. Su propia vista previa ya dice "Dropdown has duplicate ID's". Un Flow que ningún teléfono puede mostrar no debería volver PUBLISHED.