Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 | 9x 9x 9x 2x 1x 1x 2x 7x 9x 9x 9x 9x 9x 9x 9x 9x 2x 1x 1x 1x 28x 5x 5x 5x 1x 3x 1x | import {
MailChannelsClient,
MailChannelsError,
} from "@jconet-ltd/mailchannels-client";
import type { ErrorLogEntry } from "./logError";
interface MailChannelsEnv {
MAILCHANNELS_API_KEY?: string;
MAILCHANNELS_DKIM_DOMAIN?: string;
MAILCHANNELS_DKIM_SELECTOR?: string;
MAILCHANNELS_DKIM_PRIVATE_KEY?: string;
MAILCHANNELS_FROM_EMAIL?: string;
MAILCHANNELS_FROM_NAME?: string;
DEVELOPER_EMAIL?: string;
}
let missingMailChannelsWarningShown = false;
/**
* Sends a richly formatted MailChannels notification describing the runtime
* error so developers can react quickly.
*/
export async function notifyDeveloperOfError(
env: MailChannelsEnv,
entry: ErrorLogEntry,
timestamp: string
): Promise<void> {
const {
MAILCHANNELS_API_KEY,
MAILCHANNELS_DKIM_DOMAIN,
MAILCHANNELS_DKIM_SELECTOR,
MAILCHANNELS_DKIM_PRIVATE_KEY,
MAILCHANNELS_FROM_EMAIL,
MAILCHANNELS_FROM_NAME,
DEVELOPER_EMAIL,
} = env;
if (
!MAILCHANNELS_API_KEY ||
!MAILCHANNELS_DKIM_DOMAIN ||
!MAILCHANNELS_DKIM_SELECTOR ||
!MAILCHANNELS_DKIM_PRIVATE_KEY ||
!MAILCHANNELS_FROM_EMAIL ||
!DEVELOPER_EMAIL
) {
if (!missingMailChannelsWarningShown) {
console.warn(
"Missing MailChannels configuration (API key, DKIM, sender, or developer email). Skipping error notification emails."
);
missingMailChannelsWarningShown = true;
}
return;
}
const fromName = MAILCHANNELS_FROM_NAME ?? "Cleo Kick Alerts";
const status = entry.status ?? "n/a";
const contextPretty = entry.context
? safeStringify(entry.context, 2, 6000)
: "No additional context provided.";
const subject = `[Kick Bot] Error ${status} at ${timestamp}`;
const textContent = [
"Kick Bot encountered an error.",
"",
`Timestamp: ${timestamp}`,
`Status: ${status}`,
`Message: ${entry.message}`,
"",
"Context:",
contextPretty,
].join("\n");
const htmlContent = `<!doctype html>
<html><body style="font-family: Arial, sans-serif;">
<p><strong>Kick Bot encountered an error.</strong></p>
<ul>
<li><strong>Timestamp:</strong> ${escapeHtml(timestamp)}</li>
<li><strong>Status:</strong> ${escapeHtml(String(status))}</li>
<li><strong>Message:</strong> ${escapeHtml(entry.message)}</li>
</ul>
<p><strong>Context</strong></p>
<pre style="background:#f5f5f5;padding:12px;border-radius:4px;white-space:pre-wrap;word-break:break-word;">${escapeHtml(
contextPretty
)}</pre>
</body></html>`;
const client = new MailChannelsClient({
apiKey: MAILCHANNELS_API_KEY,
dkim: {
domain: MAILCHANNELS_DKIM_DOMAIN,
selector: MAILCHANNELS_DKIM_SELECTOR,
privateKey: MAILCHANNELS_DKIM_PRIVATE_KEY,
},
});
try {
await client.sendEmail({
personalizations: [
{
to: [{ email: DEVELOPER_EMAIL }],
},
],
from: {
email: MAILCHANNELS_FROM_EMAIL,
name: fromName,
},
subject,
content: [
{ type: "text/plain", value: textContent },
{ type: "text/html", value: htmlContent },
],
});
} catch (error) {
if (error instanceof MailChannelsError) {
console.error("MailChannels send failed", {
status: error.status,
requestId: error.requestId,
details: error.details,
});
return;
}
console.error(
"Unexpected error while sending MailChannels notification",
error
);
}
}
function escapeHtml(value: string): string {
return value
.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/"/g, """)
.replace(/'/g, "'");
}
function safeStringify(value: unknown, spacing = 0, maxLength = 8000): string {
try {
const json = JSON.stringify(value, null, spacing);
if (!json) {
return "{}";
}
return json.length > maxLength ? `${json.slice(0, maxLength)}…` : json;
} catch {
return String(value);
}
}
|