You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
P0-1 — Brevo auth error silently dropped all email. brevo_provider.go
classified a 401/403 (bad/expired/revoked API key) and 429 (rate limit)
as Permanent, so the forwarder advanced the cursor past every audit row
in every batch — total, silent, unrecoverable email loss. Reclassify
401/403/429 as Transient (cursor held, recoverable) and emit an
alert-able email.brevo.auth_wall ERROR. 400/422 payload rejects stay
Permanent. Test: TestBrevoProvider_AuthFailureIsTransient.
P1-2 — cursor had no time floor. A Redis wipe reset the cursor to zero
and fetchBatch (no age bound) re-sent the entire audit_log history. Add
a 48h age floor to fetchBatch and seed a missing cursor to now()-grace
instead of the zero value. Tests: TestEventForwarder_FetchBatch_HasAgeFloor,
TestEventForwarder_MissingCursor_SeedsToNow.
P1-3 — no send ledger. Idempotency rested on the Brevo X-Mailin-Custom
header, which is not a delivery-dedup mechanism. Add a forwarder_sent
ledger table (migration 055) claimed ON CONFLICT DO NOTHING before each
send; a duplicate audit_id is skipped. A Transient send releases the
claim so the retry can re-send. Tests: TestEventForwarder_Ledger_SendsOnce,
TestEventForwarder_TransientReleasesLedger.
Log noise (P1, reported symptom) — idle-tick INFO demoted to DEBUG:
event_email_forwarder.no_new_rows, middleware.work_ok, and the
zero-candidate *.completed lines in checkout_reconcile / expiry_reminder /
payment_grace_reminder / payment_grace_terminator / deployment_reminder.
INFO now only fires on non-zero work. Test: TestEventForwarder_IdleTick_NoInfoLog.
P2-1 — builder_skipped_row demoted WARN -> INFO (no owner email is an
expected state for deleted/orphan/test teams). Cursor-advance unchanged.
Test: TestEventForwarder_BuilderSkippedRow_NotWarn.
F5 — suppression bypass. The forwarder checked suppression against
row.OwnerEmail but sent to resolveRecipient(row); for anon-tier
metadata.email recipients the check ran against "" and was bypassed.
Resolve the recipient once and check suppression against the same
address that is emailed. Test: TestEventForwarder_SuppressionUsesSentRecipient.
F4 — silent no-op. A kind in eventEmailBuilders with no renderer fell
through the dead Brevo-template path -> SkippedNoTemplate -> cursor
advanced, zero email, zero error. A missing renderer is now a loud ERROR
and holds the cursor. Test: TestEventForwarder_MissingRenderer_LoudErrorNoAdvance.
F3 — deploy-reminder spam. deployment_reminder fired 6 identical emails
over 12h. Reduced to a 3-stage escalating cadence (maxDeployReminders=3)
with a reminder_index-keyed subject prefix (Heads up / Reminder / Final
reminder), matching anon.expiry_warning. Test: TestDeployExpiringSoon_EscalatingCadence.
Migration 055_forwarder_sent.sql added to worker/sql (canonical) and
mirrored into api/internal/db/migrations + api testhelpers schema mirror
(separate commit in the api repo).
go build ./... && go vet ./... && go test ./... -count=1 — all green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
t.Errorf("SendEvent on %d = nil; want SendError(Transient)", status)
182
+
continue
183
+
}
184
+
varse*SendError
185
+
if!errors.As(err, &se) {
186
+
t.Errorf("SendEvent on %d returned %T; want *SendError", status, err)
187
+
continue
188
+
}
189
+
ifse.Class!=SendClassTransient {
190
+
t.Errorf("SendEvent on %d → Class=%v; want SendClassTransient — auth/rate failure must HOLD the cursor, not advance and drop email silently", status, se.Class)
0 commit comments