generated from nhcarrigan/template
feat: Update Error Notifications with ComponentsV2 (#14)
## Summary This PR updates Rosalia's Discord error notifications to use Discord's Components V2 for a beautiful, organized display. ### Changes Made - **Removed @mention** - Error notifications no longer ping anyone - **Updated channel** - Notifications now go to the correct channel (1474606829504954511) - **Implemented ComponentsV2** - Using Container (type 17) with Text Display (type 10) components - **Visual improvements**: - Added accent color (#E91E63) for the container - Added separators (type 14) between sections - Organized content with headers for scope, message, and stack trace - Code blocks for message and stack trace ### Technical Changes 1. **`src/modules/pipeLog.ts`**: - Created `PipeErrorOptions` and `DiscordErrorPayloadOptions` interfaces - Refactored `pipeError` to accept options object - Extracted payload creation into `createDiscordErrorPayload` helper - Implemented ComponentsV2 structure with container, text displays, and separators 2. **`src/utils/errorHandler.ts`**: - Updated error handler to use new `pipeError` interface 3. **`src/server/serve.ts`**: - Updated `/error` webhook endpoint to use new interface ✨ Created by Hikari~ 🌸 Reviewed-on: #14 Co-authored-by: Hikari <hikari@nhcarrigan.com> Co-committed-by: Hikari <hikari@nhcarrigan.com>
This commit was merged in pull request #14.
This commit is contained in:
+1
-1
@@ -24,7 +24,7 @@
|
||||
"dependencies": {
|
||||
"discord-verify": "1.2.0",
|
||||
"fastify": "5.2.1",
|
||||
"fastify-raw-body": "^5.0.0",
|
||||
"fastify-raw-body": "5.0.0",
|
||||
"nodemailer": "6.10.0",
|
||||
"stripe": "18.3.0"
|
||||
}
|
||||
|
||||
Generated
+11
-1
@@ -15,7 +15,7 @@ importers:
|
||||
specifier: 5.2.1
|
||||
version: 5.2.1
|
||||
fastify-raw-body:
|
||||
specifier: ^5.0.0
|
||||
specifier: 5.0.0
|
||||
version: 5.0.0
|
||||
nodemailer:
|
||||
specifier: 6.10.0
|
||||
@@ -371,51 +371,61 @@ packages:
|
||||
resolution: {integrity: sha512-88fSzjC5xeH9S2Vg3rPgXJULkHcLYMkh8faix8DX4h4TIAL65ekwuQMA/g2CXq8W+NJC43V6fUpYZNjaX3+IIg==}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-arm-musleabihf@4.34.6':
|
||||
resolution: {integrity: sha512-wM4ztnutBqYFyvNeR7Av+reWI/enK9tDOTKNF+6Kk2Q96k9bwhDDOlnCUNRPvromlVXo04riSliMBs/Z7RteEg==}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-arm64-gnu@4.34.6':
|
||||
resolution: {integrity: sha512-9RyprECbRa9zEjXLtvvshhw4CMrRa3K+0wcp3KME0zmBe1ILmvcVHnypZ/aIDXpRyfhSYSuN4EPdCCj5Du8FIA==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-arm64-musl@4.34.6':
|
||||
resolution: {integrity: sha512-qTmklhCTyaJSB05S+iSovfo++EwnIEZxHkzv5dep4qoszUMX5Ca4WM4zAVUMbfdviLgCSQOu5oU8YoGk1s6M9Q==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-loongarch64-gnu@4.34.6':
|
||||
resolution: {integrity: sha512-4Qmkaps9yqmpjY5pvpkfOerYgKNUGzQpFxV6rnS7c/JfYbDSU0y6WpbbredB5cCpLFGJEqYX40WUmxMkwhWCjw==}
|
||||
cpu: [loong64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-powerpc64le-gnu@4.34.6':
|
||||
resolution: {integrity: sha512-Zsrtux3PuaxuBTX/zHdLaFmcofWGzaWW1scwLU3ZbW/X+hSsFbz9wDIp6XvnT7pzYRl9MezWqEqKy7ssmDEnuQ==}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-riscv64-gnu@4.34.6':
|
||||
resolution: {integrity: sha512-aK+Zp+CRM55iPrlyKiU3/zyhgzWBxLVrw2mwiQSYJRobCURb781+XstzvA8Gkjg/hbdQFuDw44aUOxVQFycrAg==}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-s390x-gnu@4.34.6':
|
||||
resolution: {integrity: sha512-WoKLVrY9ogmaYPXwTH326+ErlCIgMmsoRSx6bO+l68YgJnlOXhygDYSZe/qbUJCSiCiZAQ+tKm88NcWuUXqOzw==}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-x64-gnu@4.34.6':
|
||||
resolution: {integrity: sha512-Sht4aFvmA4ToHd2vFzwMFaQCiYm2lDFho5rPcvPBT5pCdC+GwHG6CMch4GQfmWTQ1SwRKS0dhDYb54khSrjDWw==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-x64-musl@4.34.6':
|
||||
resolution: {integrity: sha512-zmmpOQh8vXc2QITsnCiODCDGXFC8LMi64+/oPpPx5qz3pqv0s6x46ps4xoycfUiVZps5PFn1gksZzo4RGTKT+A==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-win32-arm64-msvc@4.34.6':
|
||||
resolution: {integrity: sha512-3/q1qUsO/tLqGBaD4uXsB6coVGB3usxw3qyeVb59aArCgedSF66MPdgRStUd7vbZOsko/CgVaY5fo2vkvPLWiA==}
|
||||
|
||||
@@ -129,6 +129,10 @@ export const applicationData: Record<string, { name: string; key: string }> = {
|
||||
key: "6ca3a85713dba0f9a32eb03f9682ead8e0a5ba9431d105119fd5d4e2776aeb17",
|
||||
name: "Elunara",
|
||||
},
|
||||
"1411089581058822275": {
|
||||
key: "c37c74a774458d3c54bdd8fa36ffa193dac974c33cafea836cbac826706fa102",
|
||||
name: "Sakura",
|
||||
},
|
||||
"1412945347134881862": {
|
||||
key: "3cee9e358fb0ba1c1e974248395e3cbda936e3ea499c68f8a86a95e07e013aca",
|
||||
name: "Umbrelle",
|
||||
@@ -137,32 +141,28 @@ export const applicationData: Record<string, { name: string; key: string }> = {
|
||||
key: "11809d144851d8506cbfb5a6dc2738a919d0935c383da785d1ff687887a7cd0a",
|
||||
name: "Tessara",
|
||||
},
|
||||
"1459002793179349023": {
|
||||
key: "90ab6b4be1f73708ef46b4f72141dde0254f68124f9642fabedbbfc4618d07cc",
|
||||
name: "Tesseract"
|
||||
},
|
||||
"1459007370746134691": {
|
||||
key: "8082f0f536a0bea6cdd28fab2f6e8451f927858901025b5b5d77fa39b3aaf6c3",
|
||||
name: "Ephemere"
|
||||
},
|
||||
"884082547183747153": {
|
||||
key: "5a73fdbd5fc5b56dbc8b19f61d94413189c73b6ed6bd4b92311a44ed01f67bb0",
|
||||
name: "Valerium"
|
||||
},
|
||||
"1411089581058822275": {
|
||||
key: "c37c74a774458d3c54bdd8fa36ffa193dac974c33cafea836cbac826706fa102",
|
||||
name: "Sakura"
|
||||
},
|
||||
"1433657054433771621": {
|
||||
key: "e650f6e74a6e284d8fa3417325e0bf77783a51875075e0fd8aae1b44bb733097",
|
||||
name: "Nomena"
|
||||
key: "e650f6e74a6e284d8fa3417325e0bf77783a51875075e0fd8aae1b44bb733097",
|
||||
name: "Nomena",
|
||||
},
|
||||
"1436837822656020663": {
|
||||
key: "6ef16fb0676f8021094bbc86ce138288da6461c642be0cd7521dca22fcff07ae",
|
||||
name: "Tyche"
|
||||
key: "6ef16fb0676f8021094bbc86ce138288da6461c642be0cd7521dca22fcff07ae",
|
||||
name: "Tyche",
|
||||
},
|
||||
"1438325099345346723": {
|
||||
key: "e43595045feb40714c898d4cc4be600581ce155e39dc36155e6f3e4602b55b25",
|
||||
name: "Saisoku"
|
||||
}
|
||||
key: "e43595045feb40714c898d4cc4be600581ce155e39dc36155e6f3e4602b55b25",
|
||||
name: "Saisoku",
|
||||
},
|
||||
"1459002793179349023": {
|
||||
key: "90ab6b4be1f73708ef46b4f72141dde0254f68124f9642fabedbbfc4618d07cc",
|
||||
name: "Tesseract",
|
||||
},
|
||||
"1459007370746134691": {
|
||||
key: "8082f0f536a0bea6cdd28fab2f6e8451f927858901025b5b5d77fa39b3aaf6c3",
|
||||
name: "Ephemere",
|
||||
},
|
||||
"884082547183747153": {
|
||||
key: "5a73fdbd5fc5b56dbc8b19f61d94413189c73b6ed6bd4b92311a44ed01f67bb0",
|
||||
name: "Valerium",
|
||||
},
|
||||
};
|
||||
|
||||
+133
-12
@@ -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
|
||||
* Naomi in Discord.
|
||||
* @param appName - The name of the application.
|
||||
* @param message - The message to log.
|
||||
* @param level - The level of the log, used for priority.
|
||||
* @param options - The error details including app name, level, message, scope, and stack trace.
|
||||
*/
|
||||
const pipeError = async(
|
||||
appName: string,
|
||||
message: string,
|
||||
level: string,
|
||||
): Promise<void> => {
|
||||
const pipeError = async(options: PipeErrorOptions): Promise<void> => {
|
||||
const { appName, level, message, scope, stack } = options;
|
||||
const logToken = process.env.ERROR_LOG_TOKEN;
|
||||
if (logToken === undefined) {
|
||||
return;
|
||||
@@ -65,10 +186,10 @@ const pipeError = async(
|
||||
},
|
||||
method: "POST",
|
||||
});
|
||||
await fetch(`https://discord.com/api/v10/channels/1385797320389431336/messages`, {
|
||||
body: JSON.stringify({
|
||||
content: `:warning: **${appName}** has encountered an error. <@465650873650118659> please check the logs.`,
|
||||
}),
|
||||
await fetch(`https://discord.com/api/v10/channels/1474606829504954511/messages`, {
|
||||
body: JSON.stringify(
|
||||
createDiscordErrorPayload({ appName, message, scope, stack }),
|
||||
),
|
||||
headers: {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention -- Standard header.
|
||||
"Authorization": `Bot ${process.env.DISCORD_TOKEN ?? ""}`,
|
||||
|
||||
@@ -20,8 +20,10 @@ export const sendMail = async(subject: string, body: string): Promise<void> => {
|
||||
user: "noreply@nhcarrigan.com",
|
||||
},
|
||||
host: "mail.nhcarrigan.com",
|
||||
port: 587, // UPDATED: Standard port for STARTTLS
|
||||
secure: false, // UPDATED: false means "Connect then upgrade via STARTTLS"
|
||||
// UPDATED: Standard port for STARTTLS
|
||||
port: 587,
|
||||
// UPDATED: false means "Connect then upgrade via STARTTLS"
|
||||
secure: false,
|
||||
};
|
||||
const defaults: SMTPTransport["options"] = {
|
||||
from: "noreply@nhcarrigan.com",
|
||||
@@ -33,4 +35,4 @@ export const sendMail = async(subject: string, body: string): Promise<void> => {
|
||||
subject: subject,
|
||||
text: body,
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
+7
-5
@@ -154,11 +154,13 @@ export const instantiateServer = async(): Promise<void> => {
|
||||
`[ERROR]: ${context} - ${application}`,
|
||||
`${message}\n\n${stack}`,
|
||||
);
|
||||
await pipeError(
|
||||
application,
|
||||
`${context} - ${message}\n${stack}`,
|
||||
"error",
|
||||
);
|
||||
await pipeError({
|
||||
appName: application,
|
||||
level: "error",
|
||||
message: message,
|
||||
scope: context,
|
||||
stack: stack,
|
||||
});
|
||||
await response.status(200).send({ success: true });
|
||||
} catch (error) {
|
||||
await errorHandler(error, "Error Webhook");
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* @author Naomi Carrigan
|
||||
*/
|
||||
|
||||
import { pipeLog } from "../modules/pipeLog.js";
|
||||
import { pipeError, pipeLog } from "../modules/pipeLog.js";
|
||||
import { sendMail } from "../modules/sendMail.js";
|
||||
|
||||
/**
|
||||
@@ -22,6 +22,13 @@ export const errorHandler = async(
|
||||
`[ERROR] ${context}: ${error.message}`,
|
||||
"error",
|
||||
);
|
||||
await pipeError({
|
||||
appName: "Rosalia Nightsong",
|
||||
level: "error",
|
||||
message: error.message,
|
||||
scope: context,
|
||||
stack: error.stack ?? "No stack trace available",
|
||||
});
|
||||
await sendMail(
|
||||
`[ERROR] ${context}: ${error.message}`,
|
||||
JSON.stringify(error, null, 2),
|
||||
@@ -33,5 +40,12 @@ export const errorHandler = async(
|
||||
`[ERROR] ${context}: ${JSON.stringify(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));
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user