mirror of
https://github.com/fsolla/teqo.git
synced 2026-11-13 06:43:06 +00:00
C147: Add logging to diagnose Google Calendar sync issues in production
- Add logging to readGoogleServiceAccountCredentials for missing/malformed env - Add logging to runCampaignCalendarSync when sync is skipped - Add logging to ensureGoogleCalendarPushChannel for channel status/errors - Add logging to runSyncPass for sync outcomes (only when changes occur) - Add logging to error catch blocks for better error tracking - Use payload.logger where available, console.warn for env-only context - All logs use consistent [GoogleCalendarSync] prefix - Logging is conditional to reduce noise in production
This commit is contained in:
parent
9480f2a2d7
commit
9e6a476f39
2 changed files with 153 additions and 9 deletions
116
docs/plans/c147-sync-google-calendar-producao-impl.md
Normal file
116
docs/plans/c147-sync-google-calendar-producao-impl.md
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
# Impl: Diagnóstico + fix: Google Calendar sync configurado em produção não funciona
|
||||
|
||||
Status: rascunho
|
||||
Atualizado em: 2026-08-27
|
||||
Issue: #932
|
||||
Intenção: docs/plans/c147-sync-google-calendar-producao.md
|
||||
Appetite restante: ~1–2 dias eng (herdado)
|
||||
|
||||
## Leitura da intenção
|
||||
|
||||
- **Outcome:** Com a integração configurada no Google e no Teqo, criar ou editar uma atividade em /campanha/atividades reflete no calendário do Google em minutos. O estado do sync exibido na agenda é honesto.
|
||||
- **O que NÃO negociar:** Teqo permanece a fonte da verdade; integração opcional e fail-closed; leader lockdown; sem PII desnecessária.
|
||||
- **O que reavaliar:** Todas as hipóteses de causa-raiz precisam ser testadas contra evidências de produção (logs, estado do DB, health checks).
|
||||
|
||||
## Abordagem recomendada
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
A[Coletar evidências] --> B{Diagnóstico}
|
||||
B --> C[Env/credential]
|
||||
B --> D[Calendar sharing]
|
||||
B --> E[Webhook reachability]
|
||||
B --> F[Push channel state]
|
||||
C --> G[Fix: env/cert]
|
||||
D --> H[Fix: sharing]
|
||||
E --> I[Fix: tunnel/route]
|
||||
F --> J[Fix: renewal]
|
||||
G --> K[Verificar sync]
|
||||
H --> K
|
||||
I --> K
|
||||
J --> K
|
||||
K --> L[Monitor + log]
|
||||
```
|
||||
|
||||
**Opções consideradas:**
|
||||
|
||||
- **A) Fix preventivo:** adicionar cron de renovação de push channel + health check periódico.
|
||||
- **B) Fix reativo:** corrigir causa raiz específica (env, sharing, webhook) e confiar na renovação existente via hooks.
|
||||
- **C) Híbrido:** diagnosticar + fix imediato + melhorar observabilidade para prevenir recidiva.
|
||||
|
||||
**Recomendação:** **C** — porque a causa raiz pode ser múltipla (env + sharing + webhook) e precisamos de visibilidade para não voltar ao mesmo problema. O apetite permite um fix pontual + logging melhorado.
|
||||
|
||||
**Rejeitadas:** A porque o cron não resolve env ausente ou sharing errado; B porque sem observabilidade o problema volta.
|
||||
|
||||
### Componentes / mudanças
|
||||
|
||||
- **`googleCalendarSync.ts`** (`src/utilities/googleCalendarSync.ts`): adicionar logging detalhado em `readGoogleServiceAccountCredentials` e `runCampaignCalendarSync` (capturar erros de autenticação, permissão, calendarId inválido).
|
||||
- **`googleCalendarClient.ts`** (`src/utilities/googleCalendarClient.ts`): melhorar mensagens de erro para incluir detalhes de autenticação (ex: "service account não tem acesso ao calendário").
|
||||
- **`googleCalendarSyncHooks.ts`** (`src/utilities/googleCalendarSyncHooks.ts`): garantir que erros de autenticação sejam registrados no estado `paused` com mensagem clara.
|
||||
- **`[secret]/route.ts`** (`src/app/(campaign)/campanha/agenda/google-webhook/[secret]/route.ts`): verificar se o webhook está acessível (testar com curl local + production).
|
||||
- **Environment:** verificar se `GOOGLE_CALENDAR_SERVICE_ACCOUNT_KEY` está presente e bem formado no container de produção; verificar se `NEXT_PUBLIC_SITE_URL` está correto para o webhook address.
|
||||
- **UI:** sem mudanças (pill/dialog já existem).
|
||||
- **Migration:** sem migration (schema existente).
|
||||
|
||||
### Access / Consent
|
||||
|
||||
- **Sem Consent** — não há PII novo.
|
||||
- **Access:** já existente; não muda.
|
||||
|
||||
### Dados → forma (se aplicável)
|
||||
|
||||
N/A.
|
||||
|
||||
## Fases verificáveis
|
||||
|
||||
1. **Diagnóstico** (~50% do appetite)
|
||||
- Verificar env `GOOGLE_CALENDAR_SERVICE_ACCOUNT_KEY` no container de produção (docker exec + cat /proc/1/environ).
|
||||
- Testar autenticação da service account com Google Calendar API (script simples de teste).
|
||||
- Verificar se a service account tem permissão de edição no calendário (via Google Calendar UI ou API).
|
||||
- Verificar se `calendarId` no DB corresponde a um calendário existente.
|
||||
- Testar acessibilidade do webhook (curl POST para a URL externa com headers de canal).
|
||||
- Verificar estado do push channel no DB (expiration, errors).
|
||||
- Gate: `pnpm gate:fast` (não deve quebrar nada).
|
||||
|
||||
2. **Fix** (~30% do appetite)
|
||||
- Corrigir causa raiz identificada (ex: compartilhar calendário com service account, ajustar env, corrigir webhook URL).
|
||||
- Se webhook inalcançável: verificar configuração do Cloudflare tunnel (regra de encaminhamento para POST /campanha/agenda/google-webhook/*).
|
||||
- Se push channel expirado: forçar renovação manual via `runGoogleCalendarSyncNow` (server action).
|
||||
- Adicionar logging detalhado em pontos-chave (credential read, API calls, webhook validation).
|
||||
- Gate: `pnpm gate:fast`.
|
||||
|
||||
3. **Verificação** (~20% do appetite)
|
||||
- Testar sync manual: criar atividade em /campanha/atividades, verificar se aparece no Google Calendar.
|
||||
- Verificar pill de status: deve mostrar "sincronizado" após sucesso, "pausado" com mensagem de erro clara se falhar.
|
||||
- Verificar webhook: enviar notificação de teste (ou aguardar mudança natural) e confirmar que chega.
|
||||
- Gate: `pnpm build` + `pnpm test` (unit + int).
|
||||
|
||||
## Rabbit holes / Não escopo (engenharia)
|
||||
|
||||
- **Sync bidirecional completa:** fora. Já implementada; o problema é que não está funcionando.
|
||||
- **OAuth push para múltiplos usuários:** fora. Service account é suficiente.
|
||||
- **Rate limiting no webhook:** fora de v1. O endpoint é validado por secret.
|
||||
- **Cache HTTP no feed:** fora. Google respeita `Cache-Control` mas a sync já é periódica.
|
||||
- **Migração de dados:** sem mudança de schema.
|
||||
|
||||
## Riscos e mitigação
|
||||
|
||||
- **Service account com permissões insuficientes:** Mitigação: verificar permissão "make changes to events" no calendário específico; se necessário, re-compartilhar.
|
||||
- **Webhook behind Cloudflare tunnel:** Mitigação: verificar regra de encaminhamento para POST; testar com curl externo; considerar IP whitelist do Google.
|
||||
- **Push channel expirado sem renovação:** Mitigação: a lógica de renovação já existe (`ensureGoogleCalendarPushChannel`); verificar se está sendo chamada (logging).
|
||||
- **Env malformado em produção:** Mitigação: adicionar validação no boot (já existe `readGoogleServiceAccountCredentials`); logar warning se base64 inválido.
|
||||
- **Race condition entre webhook e hooks:** Mitigação: já resolvida com CAS pattern; não muda.
|
||||
|
||||
## Qualidade da decisão
|
||||
|
||||
**Auto-avaliação: 4/5** — O plano é baseado em evidências concretas (explorer findings) e segue o padrão de debug do codebase. As hipóteses são testáveis e o apetite é realista. Pontos fracos: depende de acesso à produção para diagnóstico (pode ser limitado), e a causa raiz pode ser uma combinação de fatores (env + sharing + webhook) que torna o fix mais demorado.
|
||||
|
||||
## Aceite de engenharia
|
||||
|
||||
- [ ] Aceite de produto da intenção ainda coberto
|
||||
- [ ] Invariantes AGENTS/engineering-standards
|
||||
- [ ] Testes de domínio previstos (unit/int) onde access/write paths mudam
|
||||
- [ ] Logging melhorado para diagnóstico futuro
|
||||
- [ ] Sync manual funciona em produção (atividade → Google Calendar)
|
||||
- [ ] Pill de status reflete estado real (synced/paused/not-configured)
|
||||
- [ ] Webhook acessível e validado
|
||||
|
|
@ -149,7 +149,12 @@ export const deriveGoogleCalendarSyncStatus = (input: {
|
|||
*/
|
||||
export const readGoogleServiceAccountCredentials = (): GoogleCalendarCredentials | null => {
|
||||
const raw = process.env[GOOGLE_CALENDAR_SERVICE_ACCOUNT_KEY_ENV]
|
||||
if (!raw) return null
|
||||
if (!raw) {
|
||||
// Note: this function is called without payload context, so we use console.warn
|
||||
// In production, this warning will appear in server logs
|
||||
console.warn(`[GoogleCalendarSync] ${GOOGLE_CALENDAR_SERVICE_ACCOUNT_KEY_ENV} não está definido no ambiente.`)
|
||||
return null
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(Buffer.from(raw, 'base64').toString('utf8')) as Record<
|
||||
|
|
@ -163,8 +168,9 @@ export const readGoogleServiceAccountCredentials = (): GoogleCalendarCredentials
|
|||
) {
|
||||
return { clientEmail: parsed.client_email, privateKey: parsed.private_key }
|
||||
}
|
||||
console.warn(`[GoogleCalendarSync] ${GOOGLE_CALENDAR_SERVICE_ACCOUNT_KEY_ENV} está malformado: client_email ou private_key ausente/vazio.`)
|
||||
} catch {
|
||||
// malformed credential — treated as absent (fail-closed, no sync attempts)
|
||||
console.warn(`[GoogleCalendarSync] ${GOOGLE_CALENDAR_SERVICE_ACCOUNT_KEY_ENV} não é base64 válido ou JSON inválido.`)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
|
@ -610,6 +616,12 @@ const runSyncPass = async (
|
|||
ids: [...remoteEventIds].filter((id) => !removedEventIds.has(id)),
|
||||
})
|
||||
|
||||
// Only log if there were actual changes or errors
|
||||
if (created > 0 || updated > 0 || deleted > 0 || reverseEdits > 0) {
|
||||
payload.logger.info(
|
||||
`[GoogleCalendarSync] Passada concluída com mudanças: created=${created}, updated=${updated}, deleted=${deleted}, reverseEdits=${reverseEdits}, calendarId=${calendarId}`,
|
||||
)
|
||||
}
|
||||
return { created, updated, deleted, reverseEdits }
|
||||
}
|
||||
|
||||
|
|
@ -750,6 +762,12 @@ const ensureGoogleCalendarPushChannel = async (
|
|||
if (config.pushChannelError) {
|
||||
await recordSyncState(payload, req, { pushChannelError: null })
|
||||
}
|
||||
// Only log on first successful check or after error recovery
|
||||
if (config.pushChannelError) {
|
||||
payload.logger.info(
|
||||
`[GoogleCalendarSync] Canal de push recuperado: channelId=${config.pushChannelId}`,
|
||||
)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -798,6 +816,9 @@ const ensureGoogleCalendarPushChannel = async (
|
|||
} catch (error) {
|
||||
const message =
|
||||
error instanceof Error ? error.message : 'Erro desconhecido ao registrar o webhook do Google.'
|
||||
payload.logger.error(
|
||||
`[GoogleCalendarSync] Erro ao registrar canal de push: ${message}`,
|
||||
)
|
||||
await recordSyncState(payload, req, { pushChannelError: message.slice(0, 500) })
|
||||
}
|
||||
}
|
||||
|
|
@ -833,14 +854,18 @@ export const runCampaignCalendarSync = async (
|
|||
const at = new Date().toISOString()
|
||||
|
||||
if (!credentials || !config?.calendarId || config.disabledAt) {
|
||||
const status = deriveGoogleCalendarSyncStatus({
|
||||
hasCredential: credentials !== null,
|
||||
calendarId: config?.calendarId ?? null,
|
||||
disabledAt: config?.disabledAt ?? null,
|
||||
lastSuccessAt: config?.lastSuccessAt ?? null,
|
||||
lastErrorAt: config?.lastErrorAt ?? null,
|
||||
})
|
||||
payload.logger.warn(
|
||||
`[GoogleCalendarSync] Sincronização ignorada: credentialsPresent=${credentials !== null}, calendarId=${config?.calendarId ?? 'null'}, disabledAt=${config?.disabledAt ?? 'null'}, status=${status}`,
|
||||
)
|
||||
return {
|
||||
status: deriveGoogleCalendarSyncStatus({
|
||||
hasCredential: credentials !== null,
|
||||
calendarId: config?.calendarId ?? null,
|
||||
disabledAt: config?.disabledAt ?? null,
|
||||
lastSuccessAt: config?.lastSuccessAt ?? null,
|
||||
lastErrorAt: config?.lastErrorAt ?? null,
|
||||
}),
|
||||
status,
|
||||
created: 0,
|
||||
updated: 0,
|
||||
deleted: 0,
|
||||
|
|
@ -873,6 +898,9 @@ export const runCampaignCalendarSync = async (
|
|||
} catch (error) {
|
||||
const message =
|
||||
error instanceof Error ? error.message : 'Erro desconhecido ao sincronizar com o Google.'
|
||||
payload.logger.error(
|
||||
`[GoogleCalendarSync] Erro durante a sincronização: ${message}`,
|
||||
)
|
||||
await recordSyncState(payload, options.req, {
|
||||
lastSyncedAt: at,
|
||||
lastErrorAt: at,
|
||||
|
|
|
|||
Loading…
Reference in a new issue