feat: Update Error Notifications with ComponentsV2 #14

Merged
naomi merged 3 commits from feat/errors into main 2026-02-20 20:08:45 -08:00
7 changed files with 195 additions and 46 deletions
+1 -1
View File
@@ -24,7 +24,7 @@
"dependencies": { "dependencies": {
"discord-verify": "1.2.0", "discord-verify": "1.2.0",
"fastify": "5.2.1", "fastify": "5.2.1",
"fastify-raw-body": "^5.0.0", "fastify-raw-body": "5.0.0",
"nodemailer": "6.10.0", "nodemailer": "6.10.0",
"stripe": "18.3.0" "stripe": "18.3.0"
} }
+11 -1
View File
@@ -15,7 +15,7 @@ importers:
specifier: 5.2.1 specifier: 5.2.1
version: 5.2.1 version: 5.2.1
fastify-raw-body: fastify-raw-body:
specifier: ^5.0.0 specifier: 5.0.0
version: 5.0.0 version: 5.0.0
nodemailer: nodemailer:
specifier: 6.10.0 specifier: 6.10.0
@@ -371,51 +371,61 @@ packages:
resolution: {integrity: sha512-88fSzjC5xeH9S2Vg3rPgXJULkHcLYMkh8faix8DX4h4TIAL65ekwuQMA/g2CXq8W+NJC43V6fUpYZNjaX3+IIg==} resolution: {integrity: sha512-88fSzjC5xeH9S2Vg3rPgXJULkHcLYMkh8faix8DX4h4TIAL65ekwuQMA/g2CXq8W+NJC43V6fUpYZNjaX3+IIg==}
cpu: [arm] cpu: [arm]
os: [linux] os: [linux]
libc: [glibc]
'@rollup/rollup-linux-arm-musleabihf@4.34.6': '@rollup/rollup-linux-arm-musleabihf@4.34.6':
resolution: {integrity: sha512-wM4ztnutBqYFyvNeR7Av+reWI/enK9tDOTKNF+6Kk2Q96k9bwhDDOlnCUNRPvromlVXo04riSliMBs/Z7RteEg==} resolution: {integrity: sha512-wM4ztnutBqYFyvNeR7Av+reWI/enK9tDOTKNF+6Kk2Q96k9bwhDDOlnCUNRPvromlVXo04riSliMBs/Z7RteEg==}
cpu: [arm] cpu: [arm]
os: [linux] os: [linux]
libc: [musl]
'@rollup/rollup-linux-arm64-gnu@4.34.6': '@rollup/rollup-linux-arm64-gnu@4.34.6':
resolution: {integrity: sha512-9RyprECbRa9zEjXLtvvshhw4CMrRa3K+0wcp3KME0zmBe1ILmvcVHnypZ/aIDXpRyfhSYSuN4EPdCCj5Du8FIA==} resolution: {integrity: sha512-9RyprECbRa9zEjXLtvvshhw4CMrRa3K+0wcp3KME0zmBe1ILmvcVHnypZ/aIDXpRyfhSYSuN4EPdCCj5Du8FIA==}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
libc: [glibc]
'@rollup/rollup-linux-arm64-musl@4.34.6': '@rollup/rollup-linux-arm64-musl@4.34.6':
resolution: {integrity: sha512-qTmklhCTyaJSB05S+iSovfo++EwnIEZxHkzv5dep4qoszUMX5Ca4WM4zAVUMbfdviLgCSQOu5oU8YoGk1s6M9Q==} resolution: {integrity: sha512-qTmklhCTyaJSB05S+iSovfo++EwnIEZxHkzv5dep4qoszUMX5Ca4WM4zAVUMbfdviLgCSQOu5oU8YoGk1s6M9Q==}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
libc: [musl]
'@rollup/rollup-linux-loongarch64-gnu@4.34.6': '@rollup/rollup-linux-loongarch64-gnu@4.34.6':
resolution: {integrity: sha512-4Qmkaps9yqmpjY5pvpkfOerYgKNUGzQpFxV6rnS7c/JfYbDSU0y6WpbbredB5cCpLFGJEqYX40WUmxMkwhWCjw==} resolution: {integrity: sha512-4Qmkaps9yqmpjY5pvpkfOerYgKNUGzQpFxV6rnS7c/JfYbDSU0y6WpbbredB5cCpLFGJEqYX40WUmxMkwhWCjw==}
cpu: [loong64] cpu: [loong64]
os: [linux] os: [linux]
libc: [glibc]
'@rollup/rollup-linux-powerpc64le-gnu@4.34.6': '@rollup/rollup-linux-powerpc64le-gnu@4.34.6':
resolution: {integrity: sha512-Zsrtux3PuaxuBTX/zHdLaFmcofWGzaWW1scwLU3ZbW/X+hSsFbz9wDIp6XvnT7pzYRl9MezWqEqKy7ssmDEnuQ==} resolution: {integrity: sha512-Zsrtux3PuaxuBTX/zHdLaFmcofWGzaWW1scwLU3ZbW/X+hSsFbz9wDIp6XvnT7pzYRl9MezWqEqKy7ssmDEnuQ==}
cpu: [ppc64] cpu: [ppc64]
os: [linux] os: [linux]
libc: [glibc]
'@rollup/rollup-linux-riscv64-gnu@4.34.6': '@rollup/rollup-linux-riscv64-gnu@4.34.6':
resolution: {integrity: sha512-aK+Zp+CRM55iPrlyKiU3/zyhgzWBxLVrw2mwiQSYJRobCURb781+XstzvA8Gkjg/hbdQFuDw44aUOxVQFycrAg==} resolution: {integrity: sha512-aK+Zp+CRM55iPrlyKiU3/zyhgzWBxLVrw2mwiQSYJRobCURb781+XstzvA8Gkjg/hbdQFuDw44aUOxVQFycrAg==}
cpu: [riscv64] cpu: [riscv64]
os: [linux] os: [linux]
libc: [glibc]
'@rollup/rollup-linux-s390x-gnu@4.34.6': '@rollup/rollup-linux-s390x-gnu@4.34.6':
resolution: {integrity: sha512-WoKLVrY9ogmaYPXwTH326+ErlCIgMmsoRSx6bO+l68YgJnlOXhygDYSZe/qbUJCSiCiZAQ+tKm88NcWuUXqOzw==} resolution: {integrity: sha512-WoKLVrY9ogmaYPXwTH326+ErlCIgMmsoRSx6bO+l68YgJnlOXhygDYSZe/qbUJCSiCiZAQ+tKm88NcWuUXqOzw==}
cpu: [s390x] cpu: [s390x]
os: [linux] os: [linux]
libc: [glibc]
'@rollup/rollup-linux-x64-gnu@4.34.6': '@rollup/rollup-linux-x64-gnu@4.34.6':
resolution: {integrity: sha512-Sht4aFvmA4ToHd2vFzwMFaQCiYm2lDFho5rPcvPBT5pCdC+GwHG6CMch4GQfmWTQ1SwRKS0dhDYb54khSrjDWw==} resolution: {integrity: sha512-Sht4aFvmA4ToHd2vFzwMFaQCiYm2lDFho5rPcvPBT5pCdC+GwHG6CMch4GQfmWTQ1SwRKS0dhDYb54khSrjDWw==}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
libc: [glibc]
'@rollup/rollup-linux-x64-musl@4.34.6': '@rollup/rollup-linux-x64-musl@4.34.6':
resolution: {integrity: sha512-zmmpOQh8vXc2QITsnCiODCDGXFC8LMi64+/oPpPx5qz3pqv0s6x46ps4xoycfUiVZps5PFn1gksZzo4RGTKT+A==} resolution: {integrity: sha512-zmmpOQh8vXc2QITsnCiODCDGXFC8LMi64+/oPpPx5qz3pqv0s6x46ps4xoycfUiVZps5PFn1gksZzo4RGTKT+A==}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
libc: [musl]
'@rollup/rollup-win32-arm64-msvc@4.34.6': '@rollup/rollup-win32-arm64-msvc@4.34.6':
resolution: {integrity: sha512-3/q1qUsO/tLqGBaD4uXsB6coVGB3usxw3qyeVb59aArCgedSF66MPdgRStUd7vbZOsko/CgVaY5fo2vkvPLWiA==} resolution: {integrity: sha512-3/q1qUsO/tLqGBaD4uXsB6coVGB3usxw3qyeVb59aArCgedSF66MPdgRStUd7vbZOsko/CgVaY5fo2vkvPLWiA==}
+20 -20
View File
@@ -129,6 +129,10 @@ export const applicationData: Record<string, { name: string; key: string }> = {
key: "6ca3a85713dba0f9a32eb03f9682ead8e0a5ba9431d105119fd5d4e2776aeb17", key: "6ca3a85713dba0f9a32eb03f9682ead8e0a5ba9431d105119fd5d4e2776aeb17",
name: "Elunara", name: "Elunara",
}, },
"1411089581058822275": {
key: "c37c74a774458d3c54bdd8fa36ffa193dac974c33cafea836cbac826706fa102",
name: "Sakura",
},
"1412945347134881862": { "1412945347134881862": {
key: "3cee9e358fb0ba1c1e974248395e3cbda936e3ea499c68f8a86a95e07e013aca", key: "3cee9e358fb0ba1c1e974248395e3cbda936e3ea499c68f8a86a95e07e013aca",
name: "Umbrelle", name: "Umbrelle",
@@ -137,32 +141,28 @@ export const applicationData: Record<string, { name: string; key: string }> = {
key: "11809d144851d8506cbfb5a6dc2738a919d0935c383da785d1ff687887a7cd0a", key: "11809d144851d8506cbfb5a6dc2738a919d0935c383da785d1ff687887a7cd0a",
name: "Tessara", name: "Tessara",
}, },
"1459002793179349023": {
key: "90ab6b4be1f73708ef46b4f72141dde0254f68124f9642fabedbbfc4618d07cc",
name: "Tesseract"
},
"1459007370746134691": {
key: "8082f0f536a0bea6cdd28fab2f6e8451f927858901025b5b5d77fa39b3aaf6c3",
name: "Ephemere"
},
"884082547183747153": {
key: "5a73fdbd5fc5b56dbc8b19f61d94413189c73b6ed6bd4b92311a44ed01f67bb0",
name: "Valerium"
},
"1411089581058822275": {
key: "c37c74a774458d3c54bdd8fa36ffa193dac974c33cafea836cbac826706fa102",
name: "Sakura"
},
"1433657054433771621": { "1433657054433771621": {
key: "e650f6e74a6e284d8fa3417325e0bf77783a51875075e0fd8aae1b44bb733097", key: "e650f6e74a6e284d8fa3417325e0bf77783a51875075e0fd8aae1b44bb733097",
name: "Nomena" name: "Nomena",
}, },
"1436837822656020663": { "1436837822656020663": {
key: "6ef16fb0676f8021094bbc86ce138288da6461c642be0cd7521dca22fcff07ae", key: "6ef16fb0676f8021094bbc86ce138288da6461c642be0cd7521dca22fcff07ae",
name: "Tyche" name: "Tyche",
}, },
"1438325099345346723": { "1438325099345346723": {
key: "e43595045feb40714c898d4cc4be600581ce155e39dc36155e6f3e4602b55b25", key: "e43595045feb40714c898d4cc4be600581ce155e39dc36155e6f3e4602b55b25",
name: "Saisoku" name: "Saisoku",
} },
"1459002793179349023": {
key: "90ab6b4be1f73708ef46b4f72141dde0254f68124f9642fabedbbfc4618d07cc",
name: "Tesseract",
},
"1459007370746134691": {
key: "8082f0f536a0bea6cdd28fab2f6e8451f927858901025b5b5d77fa39b3aaf6c3",
name: "Ephemere",
},
"884082547183747153": {
key: "5a73fdbd5fc5b56dbc8b19f61d94413189c73b6ed6bd4b92311a44ed01f67bb0",
name: "Valerium",
},
}; };
+133 -12
View File
@@ -35,18 +35,139 @@ const pipeLog = async(
}); });
}; };
/**
* Options for the pipeError function.
*/
interface PipeErrorOptions {
/**
* The name of the application.
*/
appName: string;
/**
* The level of the log, used for priority.
*/
level: string;
/**
* The message to log.
*/
message: string;
/**
* The context in which the error occurred.
*/
scope: string;
/**
* The error stack trace.
*/
stack: string;
}
/**
* Options for creating the Discord error payload.
*/
interface DiscordErrorPayloadOptions {
/**
* The name of the application.
*/
appName: string;
/**
* The error message.
*/
message: string;
/**
* The context in which the error occurred.
*/
scope: string;
/**
* The error stack trace.
*/
stack: string;
}
/**
* Creates a Media Container with error header image.
* @param appName - The name of the application.
* @returns The Media Container component.
*/
const createMediaContainer = (appName: string): Record<string, unknown> => {
return {
accessory: {
description: null,
media: {
url: "https://cdn.nhcarrigan.com/error.jpeg",
},
spoiler: false,
type: 11,
},
components: [
{
content: `# Error in ${appName}`,
type: 10,
},
],
type: 9,
};
};
/**
* Creates a Discord ComponentsV2 payload with a container layout
* for beautiful error notifications.
* @param options - The error details for the payload.
* @returns The Discord message payload.
*/
const createDiscordErrorPayload = (
options: DiscordErrorPayloadOptions,
): Record<string, unknown> => {
const { appName, message, scope, stack } = options;
return {
components: [
{
// eslint-disable-next-line @typescript-eslint/naming-convention -- Discord API property.
accent_color: 15_277_667,
components: [
createMediaContainer(appName),
{
divider: true,
spacing: 1,
type: 14,
},
{
content: `## Scope\n\n${scope}`,
type: 10,
},
{
divider: true,
spacing: 1,
type: 14,
},
{
content: `## Error Details\n\n### Message\n\n\`\`\`\n${message}\n\`\`\`\n\n### Stack\n\n\`\`\`\n${stack}\n\`\`\``,
type: 10,
},
],
spoiler: false,
type: 17,
},
],
flags: 32_768,
};
};
/** /**
* Pipes a log message to the Gotify server. Also notifies * Pipes a log message to the Gotify server. Also notifies
* Naomi in Discord. * Naomi in Discord.
* @param appName - The name of the application. * @param options - The error details including app name, level, message, scope, and stack trace.
* @param message - The message to log.
* @param level - The level of the log, used for priority.
*/ */
const pipeError = async( const pipeError = async(options: PipeErrorOptions): Promise<void> => {
appName: string, const { appName, level, message, scope, stack } = options;
message: string,
level: string,
): Promise<void> => {
const logToken = process.env.ERROR_LOG_TOKEN; const logToken = process.env.ERROR_LOG_TOKEN;
if (logToken === undefined) { if (logToken === undefined) {
return; return;
@@ -65,10 +186,10 @@ const pipeError = async(
}, },
method: "POST", method: "POST",
}); });
await fetch(`https://discord.com/api/v10/channels/1385797320389431336/messages`, { await fetch(`https://discord.com/api/v10/channels/1474606829504954511/messages`, {
body: JSON.stringify({ body: JSON.stringify(
content: `:warning: **${appName}** has encountered an error. <@465650873650118659> please check the logs.`, createDiscordErrorPayload({ appName, message, scope, stack }),
}), ),
headers: { headers: {
// eslint-disable-next-line @typescript-eslint/naming-convention -- Standard header. // eslint-disable-next-line @typescript-eslint/naming-convention -- Standard header.
"Authorization": `Bot ${process.env.DISCORD_TOKEN ?? ""}`, "Authorization": `Bot ${process.env.DISCORD_TOKEN ?? ""}`,
+4 -2
View File
@@ -20,8 +20,10 @@ export const sendMail = async(subject: string, body: string): Promise<void> => {
user: "noreply@nhcarrigan.com", user: "noreply@nhcarrigan.com",
}, },
host: "mail.nhcarrigan.com", host: "mail.nhcarrigan.com",
port: 587, // UPDATED: Standard port for STARTTLS // UPDATED: Standard port for STARTTLS
secure: false, // UPDATED: false means "Connect then upgrade via STARTTLS" port: 587,
// UPDATED: false means "Connect then upgrade via STARTTLS"
secure: false,
}; };
const defaults: SMTPTransport["options"] = { const defaults: SMTPTransport["options"] = {
from: "noreply@nhcarrigan.com", from: "noreply@nhcarrigan.com",
+7 -5
View File
@@ -154,11 +154,13 @@ export const instantiateServer = async(): Promise<void> => {
`[ERROR]: ${context} - ${application}`, `[ERROR]: ${context} - ${application}`,
`${message}\n\n${stack}`, `${message}\n\n${stack}`,
); );
await pipeError( await pipeError({
application, appName: application,
`${context} - ${message}\n${stack}`, level: "error",
"error", message: message,
); scope: context,
stack: stack,
});
await response.status(200).send({ success: true }); await response.status(200).send({ success: true });
} catch (error) { } catch (error) {
await errorHandler(error, "Error Webhook"); await errorHandler(error, "Error Webhook");
+15 -1
View File
@@ -4,7 +4,7 @@
* @author Naomi Carrigan * @author Naomi Carrigan
*/ */
import { pipeLog } from "../modules/pipeLog.js"; import { pipeError, pipeLog } from "../modules/pipeLog.js";
import { sendMail } from "../modules/sendMail.js"; import { sendMail } from "../modules/sendMail.js";
/** /**
@@ -22,6 +22,13 @@ export const errorHandler = async(
`[ERROR] ${context}: ${error.message}`, `[ERROR] ${context}: ${error.message}`,
"error", "error",
); );
await pipeError({
appName: "Rosalia Nightsong",
level: "error",
message: error.message,
scope: context,
stack: error.stack ?? "No stack trace available",
});
await sendMail( await sendMail(
`[ERROR] ${context}: ${error.message}`, `[ERROR] ${context}: ${error.message}`,
JSON.stringify(error, null, 2), JSON.stringify(error, null, 2),
@@ -33,5 +40,12 @@ export const errorHandler = async(
`[ERROR] ${context}: ${JSON.stringify(error)}`, `[ERROR] ${context}: ${JSON.stringify(error)}`,
"error", "error",
); );
await pipeError({
appName: "Rosalia Nightsong",
level: "error",
message: JSON.stringify(error),
scope: context,
stack: "No stack trace available (not an Error instance)",
});
await sendMail(`[ERROR] ${context}`, JSON.stringify(error, null, 2)); await sendMail(`[ERROR] ${context}`, JSON.stringify(error, null, 2));
}; };