feat(log): improve logging for emails

This commit is contained in:
2026-03-11 14:23:39 +01:00
parent 5e5efcaf6f
commit 17aa8956a7
2 changed files with 299 additions and 93 deletions

View File

@@ -31,6 +31,25 @@ function getTransport(): nodemailer.Transporter | null {
const from = env.SMTP_FROM ?? "Kunstenkamp <info@kunstenkamp.be>";
const baseUrl = env.BETTER_AUTH_URL ?? "https://kunstenkamp.be";
// ---------------------------------------------------------------------------
// Structured logger
// ---------------------------------------------------------------------------
/**
* Emits a single-line JSON log that Cloudflare captures in its invocation
* log output (visible in the CF dashboard and via `wrangler tail`).
*
* Plain `console.warn` / `console.error` calls are NOT surfaced in the CF
* dashboard — only `console.log` with a JSON string payload is captured.
*/
export function emailLog(
level: "info" | "warn" | "error",
event: string,
extra?: Record<string, unknown>,
): void {
console.log(JSON.stringify({ level, event, ...extra }));
}
// ---------------------------------------------------------------------------
// Primitives
// ---------------------------------------------------------------------------
@@ -474,29 +493,45 @@ export async function sendConfirmationEmail(params: {
/** Pre-filled signup URL, e.g. /login?signup=1&email=…&next=/account */
signupUrl?: string;
}) {
const type = "registrationConfirmation";
emailLog("info", "email.attempt", { type, to: params.to });
const transport = getTransport();
if (!transport) {
console.warn("SMTP not configured — skipping confirmation email");
emailLog("warn", "email.skipped", {
type,
to: params.to,
reason: "smtp_not_configured",
});
return;
}
const manageUrl = `${baseUrl}/manage/${params.managementToken}`;
const signupUrl =
params.signupUrl ??
`${baseUrl}/login?signup=1&email=${encodeURIComponent(params.to)}&next=/account`;
await transport.sendMail({
from,
to: params.to,
subject: "Bevestiging inschrijving — Open Mic Night",
html: registrationConfirmationHtml({
firstName: params.firstName,
manageUrl,
signupUrl,
wantsToPerform: params.wantsToPerform,
artForm: params.artForm,
giftAmount: params.giftAmount,
drinkCardValue: params.drinkCardValue,
}),
});
try {
await transport.sendMail({
from,
to: params.to,
subject: "Bevestiging inschrijving — Open Mic Night",
html: registrationConfirmationHtml({
firstName: params.firstName,
manageUrl,
signupUrl,
wantsToPerform: params.wantsToPerform,
artForm: params.artForm,
giftAmount: params.giftAmount,
drinkCardValue: params.drinkCardValue,
}),
});
emailLog("info", "email.sent", { type, to: params.to });
} catch (err) {
emailLog("error", "email.error", {
type,
to: params.to,
error: String(err),
});
throw err;
}
}
export async function sendUpdateEmail(params: {
@@ -508,94 +543,172 @@ export async function sendUpdateEmail(params: {
giftAmount?: number;
drinkCardValue?: number;
}) {
const type = "updateConfirmation";
emailLog("info", "email.attempt", { type, to: params.to });
const transport = getTransport();
if (!transport) {
console.warn("SMTP not configured — skipping update email");
emailLog("warn", "email.skipped", {
type,
to: params.to,
reason: "smtp_not_configured",
});
return;
}
const manageUrl = `${baseUrl}/manage/${params.managementToken}`;
await transport.sendMail({
from,
to: params.to,
subject: "Inschrijving bijgewerkt — Open Mic Night",
html: updateConfirmationHtml({
firstName: params.firstName,
manageUrl,
wantsToPerform: params.wantsToPerform,
artForm: params.artForm,
giftAmount: params.giftAmount,
drinkCardValue: params.drinkCardValue,
}),
});
try {
await transport.sendMail({
from,
to: params.to,
subject: "Inschrijving bijgewerkt — Open Mic Night",
html: updateConfirmationHtml({
firstName: params.firstName,
manageUrl,
wantsToPerform: params.wantsToPerform,
artForm: params.artForm,
giftAmount: params.giftAmount,
drinkCardValue: params.drinkCardValue,
}),
});
emailLog("info", "email.sent", { type, to: params.to });
} catch (err) {
emailLog("error", "email.error", {
type,
to: params.to,
error: String(err),
});
throw err;
}
}
export async function sendCancellationEmail(params: {
to: string;
firstName: string;
}) {
const type = "cancellation";
emailLog("info", "email.attempt", { type, to: params.to });
const transport = getTransport();
if (!transport) {
console.warn("SMTP not configured — skipping cancellation email");
emailLog("warn", "email.skipped", {
type,
to: params.to,
reason: "smtp_not_configured",
});
return;
}
await transport.sendMail({
from,
to: params.to,
subject: "Inschrijving geannuleerd — Open Mic Night",
html: cancellationHtml({ firstName: params.firstName }),
});
try {
await transport.sendMail({
from,
to: params.to,
subject: "Inschrijving geannuleerd — Open Mic Night",
html: cancellationHtml({ firstName: params.firstName }),
});
emailLog("info", "email.sent", { type, to: params.to });
} catch (err) {
emailLog("error", "email.error", {
type,
to: params.to,
error: String(err),
});
throw err;
}
}
export async function sendSubscriptionConfirmationEmail(params: {
to: string;
}) {
const type = "subscriptionConfirmation";
emailLog("info", "email.attempt", { type, to: params.to });
const transport = getTransport();
if (!transport) {
console.warn(
"SMTP not configured — skipping subscription confirmation email",
);
emailLog("warn", "email.skipped", {
type,
to: params.to,
reason: "smtp_not_configured",
});
return;
}
await transport.sendMail({
from,
to: params.to,
subject: "Herinnering ingepland — Open Mic Night",
html: subscriptionConfirmationHtml({ email: params.to }),
});
try {
await transport.sendMail({
from,
to: params.to,
subject: "Herinnering ingepland — Open Mic Night",
html: subscriptionConfirmationHtml({ email: params.to }),
});
emailLog("info", "email.sent", { type, to: params.to });
} catch (err) {
emailLog("error", "email.error", {
type,
to: params.to,
error: String(err),
});
throw err;
}
}
export async function sendReminder24hEmail(params: {
to: string;
firstName?: string | null;
}) {
const type = "reminder24h";
emailLog("info", "email.attempt", { type, to: params.to });
const transport = getTransport();
if (!transport) {
console.warn("SMTP not configured — skipping 24h reminder email");
emailLog("warn", "email.skipped", {
type,
to: params.to,
reason: "smtp_not_configured",
});
return;
}
await transport.sendMail({
from,
to: params.to,
subject: "Nog 24 uur — inschrijvingen openen morgen!",
html: reminder24hHtml({ firstName: params.firstName }),
});
try {
await transport.sendMail({
from,
to: params.to,
subject: "Nog 24 uur — inschrijvingen openen morgen!",
html: reminder24hHtml({ firstName: params.firstName }),
});
emailLog("info", "email.sent", { type, to: params.to });
} catch (err) {
emailLog("error", "email.error", {
type,
to: params.to,
error: String(err),
});
throw err;
}
}
export async function sendReminderEmail(params: {
to: string;
firstName?: string | null;
}) {
const type = "reminder1h";
emailLog("info", "email.attempt", { type, to: params.to });
const transport = getTransport();
if (!transport) {
console.warn("SMTP not configured — skipping reminder email");
emailLog("warn", "email.skipped", {
type,
to: params.to,
reason: "smtp_not_configured",
});
return;
}
await transport.sendMail({
from,
to: params.to,
subject: "Nog 1 uur — inschrijvingen openen straks!",
html: reminderHtml({ firstName: params.firstName }),
});
try {
await transport.sendMail({
from,
to: params.to,
subject: "Nog 1 uur — inschrijvingen openen straks!",
html: reminderHtml({ firstName: params.firstName }),
});
emailLog("info", "email.sent", { type, to: params.to });
} catch (err) {
emailLog("error", "email.error", {
type,
to: params.to,
error: String(err),
});
throw err;
}
}
export async function sendPaymentReminderEmail(params: {
@@ -605,25 +718,41 @@ export async function sendPaymentReminderEmail(params: {
drinkCardValue?: number;
giftAmount?: number;
}) {
const type = "paymentReminder";
emailLog("info", "email.attempt", { type, to: params.to });
const transport = getTransport();
if (!transport) {
console.warn("SMTP not configured — skipping payment reminder email");
emailLog("warn", "email.skipped", {
type,
to: params.to,
reason: "smtp_not_configured",
});
return;
}
const accountUrl = `${baseUrl}/account`;
const manageUrl = `${baseUrl}/manage/${params.managementToken}`;
await transport.sendMail({
from,
to: params.to,
subject: "Herinnering: betaling open — Open Mic Night",
html: paymentReminderHtml({
firstName: params.firstName,
accountUrl,
manageUrl,
drinkCardValue: params.drinkCardValue,
giftAmount: params.giftAmount,
}),
});
try {
await transport.sendMail({
from,
to: params.to,
subject: "Herinnering: betaling open — Open Mic Night",
html: paymentReminderHtml({
firstName: params.firstName,
accountUrl,
manageUrl,
drinkCardValue: params.drinkCardValue,
giftAmount: params.giftAmount,
}),
});
emailLog("info", "email.sent", { type, to: params.to });
} catch (err) {
emailLog("error", "email.error", {
type,
to: params.to,
error: String(err),
});
throw err;
}
}
export async function sendPaymentConfirmationEmail(params: {
@@ -633,21 +762,37 @@ export async function sendPaymentConfirmationEmail(params: {
drinkCardValue?: number;
giftAmount?: number;
}) {
const type = "paymentConfirmation";
emailLog("info", "email.attempt", { type, to: params.to });
const transport = getTransport();
if (!transport) {
console.warn("SMTP not configured — skipping payment confirmation email");
emailLog("warn", "email.skipped", {
type,
to: params.to,
reason: "smtp_not_configured",
});
return;
}
const manageUrl = `${baseUrl}/manage/${params.managementToken}`;
await transport.sendMail({
from,
to: params.to,
subject: "Betaling ontvangen — Open Mic Night",
html: paymentConfirmationHtml({
firstName: params.firstName,
manageUrl,
drinkCardValue: params.drinkCardValue,
giftAmount: params.giftAmount,
}),
});
try {
await transport.sendMail({
from,
to: params.to,
subject: "Betaling ontvangen — Open Mic Night",
html: paymentConfirmationHtml({
firstName: params.firstName,
manageUrl,
drinkCardValue: params.drinkCardValue,
giftAmount: params.giftAmount,
}),
});
emailLog("info", "email.sent", { type, to: params.to });
} catch (err) {
emailLog("error", "email.error", {
type,
to: params.to,
error: String(err),
});
throw err;
}
}