Add email sent/failed structured logging
All checks were successful
Deploy / deploy (push) Successful in 1m38s

Implement email logging per observability spec:
- Add structured logging for email sent (info level) and failed (error level)
- Include userId, type, and recipient fields in log events
- Add userId parameter to email functions (sendDailyEmail, sendPeriodConfirmationEmail, sendTokenExpirationWarning)
- Update cron routes (notifications, garmin-sync) to pass userId

6 new tests added to email.test.ts (now 30 tests total)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-11 23:06:19 +00:00
parent 13b58c3c32
commit 31932a88bf
6 changed files with 224 additions and 47 deletions

View File

@@ -424,6 +424,7 @@ describe("POST /api/cron/garmin-sync", () => {
expect(mockSendTokenExpirationWarning).toHaveBeenCalledWith(
"user@example.com",
14,
"user123",
);
const body = await response.json();
expect(body.warningsSent).toBe(1);
@@ -438,6 +439,7 @@ describe("POST /api/cron/garmin-sync", () => {
expect(mockSendTokenExpirationWarning).toHaveBeenCalledWith(
"user@example.com",
7,
"user123",
);
const body = await response.json();
expect(body.warningsSent).toBe(1);
@@ -493,10 +495,12 @@ describe("POST /api/cron/garmin-sync", () => {
expect(mockSendTokenExpirationWarning).toHaveBeenCalledWith(
"user1@example.com",
14,
"user1",
);
expect(mockSendTokenExpirationWarning).toHaveBeenCalledWith(
"user2@example.com",
7,
"user2",
);
const body = await response.json();
expect(body.warningsSent).toBe(2);

View File

@@ -82,7 +82,7 @@ export async function POST(request: Request) {
const daysRemaining = daysUntilExpiry(tokens);
if (daysRemaining === 14 || daysRemaining === 7) {
try {
await sendTokenExpirationWarning(user.email, daysRemaining);
await sendTokenExpirationWarning(user.email, daysRemaining, user.id);
result.warningsSent++;
} catch {
// Continue processing even if warning email fails

View File

@@ -121,25 +121,28 @@ export async function POST(request: Request) {
const nutrition = getNutritionGuidance(dailyLog.cycleDay);
// Send email
await sendDailyEmail({
to: user.email,
cycleDay: dailyLog.cycleDay,
phase: dailyLog.phase,
decision: {
status: dailyLog.trainingDecision,
reason: dailyLog.decisionReason,
icon: getDecisionIcon(dailyLog.trainingDecision as DecisionStatus),
await sendDailyEmail(
{
to: user.email,
cycleDay: dailyLog.cycleDay,
phase: dailyLog.phase,
decision: {
status: dailyLog.trainingDecision,
reason: dailyLog.decisionReason,
icon: getDecisionIcon(dailyLog.trainingDecision as DecisionStatus),
},
bodyBatteryCurrent: dailyLog.bodyBatteryCurrent,
bodyBatteryYesterdayLow: dailyLog.bodyBatteryYesterdayLow,
hrvStatus: dailyLog.hrvStatus,
weekIntensity: dailyLog.weekIntensityMinutes,
phaseLimit: dailyLog.phaseLimit,
remainingMinutes: dailyLog.remainingMinutes,
seeds: nutrition.seeds,
carbRange: nutrition.carbRange,
ketoGuidance: nutrition.ketoGuidance,
},
bodyBatteryCurrent: dailyLog.bodyBatteryCurrent,
bodyBatteryYesterdayLow: dailyLog.bodyBatteryYesterdayLow,
hrvStatus: dailyLog.hrvStatus,
weekIntensity: dailyLog.weekIntensityMinutes,
phaseLimit: dailyLog.phaseLimit,
remainingMinutes: dailyLog.remainingMinutes,
seeds: nutrition.seeds,
carbRange: nutrition.carbRange,
ketoGuidance: nutrition.ketoGuidance,
});
user.id,
);
// Update notificationSentAt timestamp
await pb.collection("dailyLogs").update(dailyLog.id, {