integrationsbackend

Google OAuth Testing Mode: Uma Bomba Relógio de Token Expirado

· 5 min de leitura

Em 12/07/2026, a gente encontrou um problema peculiar em produção com as integrações Google de um usuário no app Lima. Tanto as integrações google_tasks quanto google_calendar estavam marcadas como EXPIRED no banco de dados. O que tornou isso particularmente complicado foi o estado dos dados associados: refresh_token e access_token ainda estavam presentes, mas last_sync_error era null. A integração simplesmente morreu sem registrar o motivo, deixando a gente no escuro.

O Problema Silencioso

A investigação inicial apontou para algumas pistas potencialmente enganosas. Os contadores de versão de “otimistic lock” das linhas estavam suspeitosamente altos: 16.662 e 14.743, respectivamente. Isso imediatamente levantou um alerta, sugerindo uma possível “retry storm” martelando essas linhas. Talvez um bug no nosso GoogleSyncScheduler estivesse constantemente tentando atualizar um token inválido e salvando a linha a cada falha, aumentando a contagem de versões.

No entanto, uma inspeção mais profunda desmentiu a hipótese da “retry storm”. Os contadores de versão estavam, na verdade, congelados, não mostrando atualizações por aproximadamente 10 horas e 6 horas. Isso significava que nenhum processo ativo estava modificando essas linhas repetidamente. Além disso, nosso GoogleSyncScheduler, que roda a cada 5 minutos, é explicitamente projetado para selecionar apenas integrações CONNECTED ou ERROR via findAllActiveByProvider. Integrações EXPIRED são deliberadamente excluídas dessa query. Os números de versão altos eram simplesmente um acúmulo histórico: aproximadamente 3 salvamentos de linha por ciclo de sincronização, multiplicados por 288 ciclos por dia, ao longo de cerca de 20 dias, resultando em uma média de 833 versões por dia. Isso era ruído operacional normal, não um sintoma do problema atual.

A Causa Técnica: Google OAuth Testing Mode

A verdadeira causa raiz era muito mais insidiosa e sutil. O refresh token para essas integrações havia sido emitido por volta de 22/06/2026, um período em que nossa tela de consentimento do Google OAuth ainda estava no modo ‘Testing’. O Google tem uma política para aplicativos no modo ‘Testing’: ele anula refresh tokens após 7 dias. Embora o MoWave One tenha publicado o aplicativo ‘In production’ em 05/07/2026, o token em questão já estava vencido. Cada tentativa subsequente de atualizar o token falhou com um erro invalid_grant e, como nosso sistema não estava registrando mensagens de erro específicas no momento da expiração, a integração simplesmente mudou para EXPIRED sem nenhuma indicação do porquê.

A Correção: Logar o Motivo da Falha

A correção imediata envolveu melhorar nosso logging de erros. Anteriormente, Integration.markExpired() apenas definia status=EXPIRED. A gente modificou para markExpired(reason), que agora escreve causas legíveis por humanos no campo last_sync_error. Por exemplo, um refresh token falho agora vai logar ‘Refresh token rejected by Google (invalid_grant), reconnect the integration’ do nosso método doRefresh. Da mesma forma, uma variante 401 do PullSyncService também preencherá este campo.

Esse design preserva deliberadamente o comportamento existente, onde os tokens nunca são apagados na expiração. O status muda para EXPIRED (não DISCONNECTED), e um IntegrationDisconnectedEvent é disparado apenas na primeira transição para EXPIRED para evitar spam de notificação. O usuário então vê um botão ‘Reconnect’ no aplicativo Lima, pedindo para ele reautenticar e obter um novo token. Isso evita perda de dados, enquanto comunica claramente a necessidade de ação do usuário.

A gente cobriu este novo comportamento em nossa suíte de testes de integração (194 testes), especificamente afirmando que last_sync_error é preenchido corretamente na transição. Nosso IntegrationService agora possui 97% de cobertura de linhas e 95% de branches, garantindo que este logging crítico seja robusto.

-- Exemplo do tipo de query que a gente estava rodando para debugar
SELECT
    id,
    user_id,
    provider_type,
    status,
    last_sync_error,
    last_sync_at,
    version
FROM
    integrations
WHERE
    status = 'EXPIRED'
AND
    last_sync_error IS NULL;

Aprendizados

Este incidente serviu como um lembrete severo: quando uma dependência de terceiros falha, é absolutamente essencial persistir o motivo no local da falha. Uma simples flag de status sem uma causa pode transformar um diagnóstico de 5 minutos em uma sessão de arqueologia de banco de dados de várias horas. Além disso, o modo ‘Testing’ do Google OAuth é uma bomba-relógio para qualquer token emitido antes de você publicar oficialmente seu aplicativo. Fique atento a quando os tokens são emitidos em relação ao status de publicação do seu app. Sempre registre erros explícitos, não apenas mudanças de status.