Skip to content

Email sender: log duration, messageId and outcome for every OTP send #183

Description

@aspiers

Problem

We are investigating production complaints of OTP emails arriving after the 10-minute expiry. The affected sends are from trusted branded clients, so production logs (default level info) do show the existing success line per send:

logger.info({ to, clientId: opts.clientId }, 'Sent client-branded OTP email')

(packages/auth-service/src/email/sender.ts:135)

But that line is not enough to diagnose delivery delay:

  • No timing: nothing records how long the SMTP handoff to Resend took, so a slow-but-successful sendMail() is indistinguishable from a fast one. (The log line's own timestamp is after the handoff; there is no "start" reference in the logs — the closest proxy is the OTP row's createdAt in the better-auth DB.)
  • The nodemailer sendMail() result — including messageId and the server response — is discarded, so the line cannot be correlated with Resend-side delivery events (email.delivered / email.delivery_delayed).
  • No explicit timeouts are configured on the SMTP transport (sender.ts:46-53); we silently inherit nodemailer defaults (connectionTimeout 2 min, greetingTimeout 30 s).

Proposed change

Constraints: augment the existing per-send line; do not add extra per-email log lines for the branded path — one line per successful send, as today, just richer.

  • Measure elapsed time around transporter.sendMail() and capture its result, then fold into the existing success line: { to, clientId, elapsedMs, messageId, smtpResponse? }.
  • On failure, include elapsedMs in the existing error path (better-auth.ts:229 logs the error already; measuring inside sendOtpCode lets that line carry the duration — still one line per failure).
  • Default (non-branded) template paths currently log nothing on success; give them the same single enriched completion line. This adds no noise to branded sends and closes the observability gap for the fallback path.
  • Optionally set explicit connectionTimeout / greetingTimeout / socketTimeout on the transport so hangs surface as errors on a known schedule instead of nodemailer's defaults.

Why

With duration and messageId on the existing line, we can (a) rule the SMTP handoff in or out as the slow step, and (b) join app logs to Resend's per-message delivery events to measure how often delivery latency exceeds the 10-minute OTP lifetime — separating genuine provider delay from users submitting stale codes (companion issue #184).

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Fields

No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions