Add email sent/failed structured logging
All checks were successful
Deploy / deploy (push) Successful in 1m38s
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:
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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, {
|
||||
|
||||
Reference in New Issue
Block a user