Em 5 de agosto eu passei o dia em cima de um fluxo de Instagram que faz uma coisa simples: alguém comenta uma palavra-chave num post, o robô responde no comentário e manda um direct com um botão. Achei três bugs. Nenhum dos três deu erro. Nenhum apareceu em log vermelho. Nenhum quebrou o fluxo. Os três mudavam o resultado para a pessoa do outro lado.

Esse é o tipo de bug que me tira o sono, e não é o que a maioria das pessoas imagina quando pensa em "a automação quebrou". A automação que quebra alto é fácil: ela avisa. A que responde sucesso e entrega errado só aparece quando alguém reclama, e a essa altura já rodou algumas centenas de vezes.

Bug 1: a pergunta que a API respondeu sem responder

O fluxo precisa saber se quem comentou já segue a conta. Se segue, manda o material. Se não segue, manda um pedido educado para seguir primeiro. A chamada estava assim:

GET https://graph.instagram.com/{ID_DO_USUARIO}
Authorization: Bearer {token}

E o nó seguinte decidia o caminho lendo is_user_follow_business da resposta.

Essa chamada devolve 200. Sempre. Ela é uma requisição perfeitamente válida. O que ela devolve é basicamente o id do usuário e nada mais, porque o parâmetro fields não foi passado. A documentação da Graph API é clara sobre isso: você especifica os campos que quer com o parâmetro fields, e isso "sobrescreve os padrões e retorna apenas os campos especificados, mais o ID do objeto, que é sempre retornado". Sem fields, você recebe o conjunto padrão, e is_user_follow_business não está nele.

O exemplo na documentação da User Profile API mostra exatamente o que faltava:

GET https://graph.facebook.com/v25.0/{ID_DO_USUARIO}
  ?fields=name,username,profile_pic,follower_count,is_user_follow_business,is_business_follow_user
  &access_token={token}

Então o campo chegava indefinido, a condição "é seguidor?" nunca dava verdadeiro, e todo mundo caía na ramificação de "você precisa me seguir para receber o material". Inclusive quem já seguia. A pessoa clicava em seguir, já estava seguindo, comentava de novo, recebia a mesma mensagem, e desistia.

Do lado do monitoramento estava tudo verde. Requisição 200, fluxo concluído, zero execuções com erro. O bug morava na diferença entre "a API respondeu" e "a API respondeu o que eu perguntei", e essas duas coisas não se parecem nada em um painel de execuções.

Bug 2: a Meta aceitou e jogou fora

O direct que sai depois leva um botão. No corpo da mensagem, isso é um quick_replies:

{
  "recipient": { "comment_id": "..." },
  "message": {
    "text": "Clique no botão abaixo para liberar o acesso",
    "quick_replies": [
      { "content_type": "text", "title": "...", "payload": "LIBERAR_ACESSO" }
    ]
  }
}

A mensagem chegou. O botão não. A API respondeu 200, com um ID de mensagem, sem nenhum aviso de que uma parte do que eu mandei tinha sido descartada.

Fui procurar na documentação o que eu tinha feito de errado. A página de quick replies lista quatro limitações: no máximo 13 quick replies, cada título truncado em 20 caracteres, só texto puro, e o recurso não está disponível no desktop. Nenhuma delas explica o caso. O que aconteceu aqui é que o destinatário não é uma conversa comum, é um comment_id, ou seja, uma resposta privada a um comentário. Isso é um caminho diferente por dentro, e ele engoliu o quick_replies sem reclamar.

Guardo esse como o mais desonesto dos três. No primeiro, eu perguntei errado. Aqui eu perguntei certo, a plataforma respondeu que estava tudo bem, e entregou menos do que confirmou. A única forma de descobrir foi abrir o Instagram no celular e olhar a mensagem que chegou. Nenhum teste automático que eu escrevesse contra a resposta da API teria pegado isso, porque a resposta da API estava correta.

Bug 3: o dado do cliente com uma quebra de linha

O terceiro é o mais bobo e o mais perigoso, porque ele fica adormecido até alguém preencher um campo de um jeito perfeitamente razoável.

O corpo do direct é montado com interpolação de variável dentro de aspas, assim:

"title": "{{ $json.palavras }}"

Isso funciona enquanto o valor for AUTOMAÇÃO. No banco desse fluxo tem um campo vizinho, mensagem_direct, que guarda o texto que a pessoa recebe. O texto real tem três parágrafos, com linha em branco entre eles, porque foi escrito para ser lido, não para ser concatenado. No momento em que um valor com quebra de linha entra numa string montada com aspas na mão, o JSON deixa de ser JSON.

A RFC 8259 não deixa margem: "todos os caracteres Unicode podem ser colocados entre aspas, exceto os que DEVEM ser escapados: aspas, barra invertida, e os caracteres de controle (U+0000 até U+001F)". Quebra de linha é U+000A. Ela está nessa lista.

O sintoma não é bonito. Dependendo de onde a quebra cai, você recebe um erro de parse que aponta para uma linha que não existe no seu editor, ou, pior, o corpo continua sendo JSON válido e passa a significar outra coisa. Aspas no meio do texto do cliente fazem o mesmo estrago.

A correção é uma linha e não tem discussão: nunca montar JSON com aspas na mão. Deixa quem sabe escapar fazer o escape.

"title": {{ JSON.stringify($json.palavras) }}

Repare que as aspas saíram. Elas agora vêm de dentro do JSON.stringify, junto com o escape de tudo que precisar ser escapado.

O que os três têm em comum

Nenhum dos três aparece em teste feito com o dado que eu inventei para testar. Eu testo com AUTOMAÇÃO, uma palavra sem acento problemático, sem quebra de linha, com nove caracteres, e com a minha própria conta, que segue a conta da empresa. Os três bugs precisam do mundo real para acordar: um usuário que ainda não segue, um destinatário que é comentário e não conversa, e um texto escrito por gente.

E os três compartilham o mesmo disfarce. Eles retornam sucesso. Se o seu critério de "está funcionando" é o painel sem vermelho, os três passam. É por isso que eu não confio em painel de execução como prova de nada além de "o processo não morreu".

O que eu passei a fazer, e que teria pego os três em minutos:

Conferir a resposta, não o código de status. Depois de qualquer chamada que alimenta uma decisão, olhar o corpo que voltou e confirmar que o campo que você vai ler está lá. Se o campo que decide o caminho vier indefinido, isso tem que ser um erro alto, não um caminho falso silencioso.

Testar com o valor mais feio que o banco aceita. Não com o valor que você escreveu para testar. Um texto de três parágrafos com acento, aspas e emoji é um teste melhor do que dez casos limpos.

Olhar a entrega do lado do usuário pelo menos uma vez. Abrir o app e ver a mensagem que chegou. É o único jeito de descobrir o que a plataforma aceitou e não entregou.

Nunca montar JSON com aspas na mão. Nenhuma exceção, nem quando o valor "sempre vai ser uma palavra só".

Nada disso é sofisticado. É a diferença entre uma automação que você acredita que funciona e uma que você sabe por que funciona.

Se você tem um fluxo rodando hoje e não sabe dizer qual das duas é a sua, me manda uma mensagem. Eu olho o que já está no ar antes de propor refazer qualquer coisa.