From 9db206ebbccc4d043711c43155cd4fb9fa2f1fbb Mon Sep 17 00:00:00 2001 From: Hikari Date: Fri, 20 Feb 2026 20:08:45 -0800 Subject: [PATCH] feat: Update Error Notifications with ComponentsV2 (#14) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 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: https://git.nhcarrigan.com/nhcarrigan/rosalia-nightsong/pulls/14 Co-authored-by: Hikari Co-committed-by: Hikari --- package.json | 2 +- pnpm-lock.yaml | 12 ++- src/config/applicationData.ts | 46 +++++------ src/modules/pipeLog.ts | 145 +++++++++++++++++++++++++++++++--- src/modules/sendMail.ts | 8 +- src/server/serve.ts | 12 +-- src/utils/errorHandler.ts | 16 +++- 7 files changed, 195 insertions(+), 46 deletions(-) diff --git a/package.json b/package.json index 6a69f4e..68ce522 100644 --- a/package.json +++ b/package.json @@ -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" } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2e9e95c..0a07e6c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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==} diff --git a/src/config/applicationData.ts b/src/config/applicationData.ts index 8dbd728..ac3bd0e 100644 --- a/src/config/applicationData.ts +++ b/src/config/applicationData.ts @@ -129,6 +129,10 @@ export const applicationData: Record = { key: "6ca3a85713dba0f9a32eb03f9682ead8e0a5ba9431d105119fd5d4e2776aeb17", name: "Elunara", }, + "1411089581058822275": { + key: "c37c74a774458d3c54bdd8fa36ffa193dac974c33cafea836cbac826706fa102", + name: "Sakura", + }, "1412945347134881862": { key: "3cee9e358fb0ba1c1e974248395e3cbda936e3ea499c68f8a86a95e07e013aca", name: "Umbrelle", @@ -137,32 +141,28 @@ export const applicationData: Record = { 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", + }, }; diff --git a/src/modules/pipeLog.ts b/src/modules/pipeLog.ts index 5c201a6..cd4f1da 100644 --- a/src/modules/pipeLog.ts +++ b/src/modules/pipeLog.ts @@ -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 => { + 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 => { + 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 => { +const pipeError = async(options: PipeErrorOptions): Promise => { + 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 ?? ""}`, diff --git a/src/modules/sendMail.ts b/src/modules/sendMail.ts index e4389fa..e0ac28b 100644 --- a/src/modules/sendMail.ts +++ b/src/modules/sendMail.ts @@ -20,8 +20,10 @@ export const sendMail = async(subject: string, body: string): Promise => { 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 => { subject: subject, text: body, }); -}; \ No newline at end of file +}; diff --git a/src/server/serve.ts b/src/server/serve.ts index 20f2aae..42d2ecf 100644 --- a/src/server/serve.ts +++ b/src/server/serve.ts @@ -154,11 +154,13 @@ export const instantiateServer = async(): Promise => { `[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"); diff --git a/src/utils/errorHandler.ts b/src/utils/errorHandler.ts index d50ebdf..37f038e 100644 --- a/src/utils/errorHandler.ts +++ b/src/utils/errorHandler.ts @@ -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)); };