Diagnóstico no ping de reunião

TLDR: anexar um payload de diagnóstico versionado (dispositivo, rede, qualidade Twilio) ao ping de reunião existente, para que problemas de conexão durante uma chamada possam ser atribuídos à rede do usuário, ao nosso servidor ou ao Twilio.

Contexto

Quando uma reunião falha ou tem qualidade ruim, não temos dados estruturados para responder: “o problema foi a internet do usuário, nosso servidor, ou o Twilio?”. O backend (chat.trg.club) já foi atualizado nesta branch para aceitar um payload de diagnóstico no corpo do ping. O frontend ainda envia um PUT vazio. usePingMeeting existe mas não está conectado a nenhum container.

Objetivos

  • Conectar usePingMeeting em Call.tsx para que o ping de fato dispare durante as chamadas
  • Construir um payload de diagnóstico versionado no cliente e enviá-lo no corpo do ping
  • Habilitar a Network Quality API do Twilio para obter um nível de qualidade (0–5) por participante
  • Amostrar estatísticas WebRTC via room.getStats() em baixa frequência para evitar sobrecarga da chamada
  • Capturar eventos de conexão (códigos 53001/53405) já logados em connectionFactory.ts
  • Fornecer fallbacks seguros para APIs ausentes no Safari (ex.: navigator.connection)

Fora de escopo

— (não registrado no documento original)

Mudanças

Arquivo O que muda
src/hooks/usePingMeeting.ts Aceita callback getDiagnostics; inclui payload no corpo do PUT
src/infra/CallManager/strategies/twilio/factories/connectionFactory.ts Adiciona networkQuality: { local: 1, remote: 1 } ao twilioConnect; expõe getNetworkStats() e eventos de conexão recentes
src/infra/CallManager/CallManager.ts Expõe getNetworkStats() do connectionFactory na API pública
src/infra/diagnostics/buildDiagnosticsPayload.ts Novo utilitário: monta o payload versionado a partir de APIs do browser + stats do Twilio
src/containers/Call/Call.tsx Conecta usePingMeeting com o callback de diagnóstico

Payload Schema

typescript { diagnostics: { timestamp: string, // ISO 8601 (client-side; não faz parte do schema documentado do backend, mantido para depurar clock drift contra o pinged_at do servidor) device: { user_agent: string, // navigator.userAgent }, network: { online: boolean, // navigator.onLine effective_type: string | null, // '4g' | '3g' | '2g' | 'slow-2g'; null quando navigator.connection não está disponível (Safari) downlink: number | null, // Mbps rtt: number | null, // ms save_data: boolean, }, app_state: { visibility_state: 'visible' | 'hidden', // document.visibilityState }, media_devices: { audioinput_count: number, audiooutput_count: number, videoinput_count: number, }, twilio: { network_quality_level: number | null, // 0–5; null se ainda não disponível network_quality_stats: object | null, // NetworkQualityStats bruto do twilio-video; null abaixo do nível de verbosidade 2 reconnection_events: Array<{ code: number; message: string; ts: string }>, }, }, }

Tudo isso é embrulhado sob uma chave diagnostics no corpo do PUT, casando com os nomes das chaves de topo do próprio spec do backend (trgclub-api .project/specs/20260729143000_meeting_ping_diagnostics.md): device, network, app_state, media_devices, twilio. Nenhum campo schema_version é enviado.

Lacunas conhecidas vs. o schema documentado do backend — alinhadas em nome mas não em formato, já que fechar a lacuna exigiria nova coleta de dados, não apenas renomear: - media_devices no backend é documentado como por-dispositivo { camera: { label, device_id }, microphone: {...}, audio_output: {...} }. O frontend só envia contagens agregadas (audioinput_count, etc.) — nunca pediu labels/IDs de dispositivo via enumerateDevices(). - twilio.network_quality_stats é documentado como um formato normalizado { audio: { send, recv }, video: { send, recv } }. O frontend repassa o objeto bruto do SDK do Twilio como está, sem normalizar. - twilio.room_stats e twilio.errors (como campo distinto de reconnection_events) não são produzidos pelo frontend de forma alguma. - Nada disso quebra nada hoje — o controller do backend usa to_unsafe_h sem whitelist de chaves/validação de formato, então qualquer hash é aceito e persistido como está.

Estratégia de amostragem: a qualidade de rede é lida a cada intervalo de ping (20 s). As estatísticas WebRTC (room.getStats()) são amostradas uma vez por minuto e cacheadas; o valor cacheado é anexado ao próximo ping. Isso evita chamar getStats() 3× por minuto.

Plano de implementação

  1. test: buildDiagnosticsPayload — cobre: navigator.connection presente, ausente (Safari), offline, aba oculta, contagem de dispositivos, stats do twilio mesclados — src/infra/diagnostics/__tests__/buildDiagnosticsPayload.test.ts

  2. feat: criar src/infra/diagnostics/buildDiagnosticsPayload.ts — lê APIs do browser com fallbacks seguros, mescla o argumento de stats do twilio no payload versionado

  3. test: qualidade de rede do connectionFactory — cobre: opção networkQuality passada ao twilioConnect, getNetworkStats retorna nível + stats, eventos de erro anexados ao buffer, buffer limpo no flush — src/infra/CallManager/strategies/twilio/factories/__tests__/connectionFactory.test.ts

  4. feat: atualizar connectionFactory.ts — adicionar networkQuality: { local: 1, remote: 1 } às opções de connect; bufferizar códigos de erro reconnecting; expor getNetworkStats() que retorna { level, stats, recentErrors } e limpa o buffer de erros

  5. feat: expor getNetworkStats na API pública do CallManager — src/infra/CallManager/CallManager.ts

  6. test: usePingMeeting com payload — cobre: corpo do PUT inclui payload serializado, payload vazio ainda envia, retry false — src/hooks/__tests__/usePingMeeting.test.ts

  7. feat: atualizar usePingMeeting.ts — aceitar getDiagnostics?: () => DiagnosticsPayload opcional, chamá-lo no queryFn, passar o resultado como corpo JSON do PUT

  8. feat: conectar usePingMeeting em Call.tsx — passar um callback getDiagnostics que chama buildDiagnosticsPayload com callManager.getNetworkStats()

Como verificar

  1. Abrir uma chamada; abrir DevTools → Network → filtrar por pings
  2. O corpo da requisição PUT deve ser um objeto JSON com uma chave diagnostics de topo (sem campo schema_version, em nenhum lugar)
  3. diagnostics.twilio.network_quality_level deve ser um número 0–5 após ~5 s de chamada
  4. diagnostics.network.effective_type deve ser null no Safari; deve ter um valor (ex.: "4g") no Chrome
  5. Simular offline (navigator.onLine = false no console) e confirmar que o próximo ping mostra diagnostics.network.online: false
  6. Verificar logs do backend (chat.trg.club) / MeetingParticipantPing.last.diagnostics para confirmar que o payload é recebido e persistido sob as chaves esperadas

Decisões e trade-offs

  • networkQuality: { local: 1, remote: 1 } (nível de verbosidade 1) — dá o inteiro de nível de qualidade sem o detalhamento por-track completo que o nível 3 produz; suficiente para triagem e baixo overhead
  • Stats WebRTC amostradas a 60 s — room.getStats() é assíncrono e envolve round-trip ao SDK do Twilio; chamar a cada 20 s significaria 3 chamadas/min por participante. Cachear e anexar ao próximo ping disponível é o trade-off certo
  • Buffer de erros limpo na leitura — cada ping recebe os erros acumulados desde o último ping, sem duplicatas
  • usePingMeeting ainda não conectado — esta mudança é a primeira vez que ele dispara em produção; vale destacar no PR
  • LGPD: navigator.userAgent e contagem de dispositivos são dados funcionais/operacionais, não pessoais; navigator.connection é metadado de rede. Nenhum PII é coletado

Documentação

Atualizar .project/docs/rules/onboarding/pro_bono_step_renders_anjos_e_tutelados.md apenas se o diagrama de fluxo de chamada mudar (referência preservada do documento original — não verificada nesta migração). Fora isso: nenhuma mudança de documentação necessária para este spec; o payload schema acima é a referência autoritativa.