diff --git a/.changeset/goofy-needles-stay.md b/.changeset/goofy-needles-stay.md new file mode 100644 index 0000000..4155726 --- /dev/null +++ b/.changeset/goofy-needles-stay.md @@ -0,0 +1,8 @@ +--- +"@embedly/builder": patch +"@embedly/logging": patch +"@embedly/api": patch +"@embedly/bot": patch +--- + +add structured error logging with error message and stack trace to all span catch blocks across bot and API diff --git a/apps/api/src/main.ts b/apps/api/src/main.ts index 4cb36b6..f804f08 100644 --- a/apps/api/src/main.ts +++ b/apps/api/src/main.ts @@ -3,6 +3,7 @@ import { EMBEDLY_CACHING_POST, EMBEDLY_NO_LINK_IN_MESSAGE, EMBEDLY_NO_VALID_LINK, + EMBEDLY_PARSE_POST_ID_FAILED, type EmbedlyPostContext, type FormattedLog, formatLog @@ -216,6 +217,14 @@ const app = (env: Env, ctx: ExecutionContext) => message: error.message }); root_span.recordException(error); + logger.error( + formatLog(EMBEDLY_PARSE_POST_ID_FAILED, { + platform: handler.name, + post_url: url, + error_message: error.message, + error_stack: error.stack + }) + ); root_span.end(); return status(400, { type: "EMBEDLY_INVALID_URL", diff --git a/apps/bot/src/commands/embed.ts b/apps/bot/src/commands/embed.ts index 9dddf15..4150884 100644 --- a/apps/bot/src/commands/embed.ts +++ b/apps/bot/src/commands/embed.ts @@ -6,11 +6,14 @@ import { type EmbedFlags } from "@embedly/builder"; import { + EMBEDLY_CREATE_EMBED_FAILED, EMBEDLY_EMBED_CREATED_COMMAND, EMBEDLY_NO_LINK_IN_MESSAGE, EMBEDLY_NO_LINK_WARN, EMBEDLY_NO_VALID_LINK, EMBEDLY_NO_VALID_LINK_WARN, + EMBEDLY_SEND_MESSAGE_FAILED, + EMBEDLY_UNHANDLED_ERROR, type EmbedlyInteractionContext, type EmbedlySource, formatDiscord, @@ -206,26 +209,66 @@ export class EmbedCommand extends Command { "create_embed", async (s) => { s.setAttribute("embedly.platform", platform.type); - const embed = await Platforms[platform.type].createEmbed(data); - s.end(); - return embed; + try { + const embed = + await Platforms[platform.type].createEmbed(data); + return embed; + } catch (error: any) { + s.setStatus({ + code: SpanStatusCode.ERROR, + message: error.message ?? String(error) + }); + s.recordException(error); + this.container.logger.error( + formatLog(EMBEDLY_CREATE_EMBED_FAILED, { + interaction_id: interaction.id, + user_id: interaction.user.id, + source, + platform: platform.type, + error_message: error.message, + error_stack: error.stack + }) + ); + throw error; + } finally { + s.end(); + } } ); const bot_message = await this.container.tracer.startActiveSpan( "send_message", async (s) => { - const res = await interaction.editReply({ - components: [Embed.getDiscordEmbed(embed, flags)!], - flags: ["IsComponentsV2"], - allowedMentions: { - parse: [], - repliedUser: false - } - }); - s.setAttribute("discord.bot_message_id", res.id); - s.end(); - return res; + try { + const res = await interaction.editReply({ + components: [Embed.getDiscordEmbed(embed, flags)!], + flags: ["IsComponentsV2"], + allowedMentions: { + parse: [], + repliedUser: false + } + }); + s.setAttribute("discord.bot_message_id", res.id); + return res; + } catch (error: any) { + s.setStatus({ + code: SpanStatusCode.ERROR, + message: error.message ?? String(error) + }); + s.recordException(error); + this.container.logger.error( + formatLog(EMBEDLY_SEND_MESSAGE_FAILED, { + interaction_id: interaction.id, + user_id: interaction.user.id, + source, + error_message: error.message, + error_stack: error.stack + }) + ); + throw error; + } finally { + s.end(); + } } ); @@ -274,6 +317,15 @@ export class EmbedCommand extends Command { message: error.message }); root_span.recordException(error); + this.container.logger.error( + formatLog(EMBEDLY_UNHANDLED_ERROR, { + interaction_id: interaction.id, + user_id: interaction.user.id, + source: "context_menu", + error_message: error.message, + error_stack: error.stack + }) + ); } finally { root_span.end(); } @@ -325,6 +377,15 @@ export class EmbedCommand extends Command { message: error.message }); root_span.recordException(error); + this.container.logger.error( + formatLog(EMBEDLY_UNHANDLED_ERROR, { + interaction_id: interaction.id, + user_id: interaction.user.id, + source: "command", + error_message: error.message, + error_stack: error.stack + }) + ); } finally { root_span.end(); } diff --git a/apps/bot/src/listeners/messageCreate.ts b/apps/bot/src/listeners/messageCreate.ts index 89133a4..a535a36 100644 --- a/apps/bot/src/listeners/messageCreate.ts +++ b/apps/bot/src/listeners/messageCreate.ts @@ -6,7 +6,10 @@ import { type EmbedFlags } from "@embedly/builder"; import { + EMBEDLY_CREATE_EMBED_FAILED, EMBEDLY_EMBED_CREATED_MESSAGE, + EMBEDLY_SEND_MESSAGE_FAILED, + EMBEDLY_UNHANDLED_ERROR, type EmbedlyInteractionContext, type EmbedlyPostContext, formatLog @@ -145,10 +148,30 @@ export class MessageListener extends Listener< "create_embed", async (s) => { s.setAttribute("embedly.platform", platform.type); - const embed = - await Platforms[platform.type].createEmbed(data); - s.end(); - return embed; + try { + const embed = + await Platforms[platform.type].createEmbed(data); + return embed; + } catch (error: any) { + s.setStatus({ + code: SpanStatusCode.ERROR, + message: error.message ?? String(error) + }); + s.recordException(error); + this.container.logger.error( + formatLog(EMBEDLY_CREATE_EMBED_FAILED, { + message_id: message.id, + user_id: message.author.id, + source: "message", + platform: platform.type, + error_message: error.message, + error_stack: error.stack + }) + ); + throw error; + } finally { + s.end(); + } } ); @@ -184,13 +207,32 @@ export class MessageListener extends Listener< await this.container.tracer.startActiveSpan( "send_message", async (s) => { - const res = - ind > 0 && message.channel.isSendable() - ? await message.channel.send(msg) - : await message.reply(msg); - s.setAttribute("discord.bot_message_id", res.id); - s.end(); - return res; + try { + const res = + ind > 0 && message.channel.isSendable() + ? await message.channel.send(msg) + : await message.reply(msg); + s.setAttribute("discord.bot_message_id", res.id); + return res; + } catch (error: any) { + s.setStatus({ + code: SpanStatusCode.ERROR, + message: error.message ?? String(error) + }); + s.recordException(error); + this.container.logger.error( + formatLog(EMBEDLY_SEND_MESSAGE_FAILED, { + message_id: message.id, + user_id: message.author.id, + source: "message", + error_message: error.message, + error_stack: error.stack + }) + ); + throw error; + } finally { + s.end(); + } } ); this.container.embed_authors.set( @@ -222,6 +264,15 @@ export class MessageListener extends Listener< message: error.message }); root_span.recordException(error); + this.container.logger.error( + formatLog(EMBEDLY_UNHANDLED_ERROR, { + message_id: message.id, + user_id: message.author.id, + source: "message", + error_message: error.message, + error_stack: error.stack + }) + ); } finally { root_span.end(); } diff --git a/packages/builder/src/Embed.ts b/packages/builder/src/Embed.ts index 33b2e7b..e3a0a36 100644 --- a/packages/builder/src/Embed.ts +++ b/packages/builder/src/Embed.ts @@ -326,7 +326,10 @@ export class Embed implements EmbedData { prefix_emoji: string ): string { const username_part = username - ? ` (${hyperlink(`@${escapeMarkdown(username, { italic: true, underline: true })}`, profile_url!)})` + ? ` (${hyperlink( + `@${escapeMarkdown(username, { italic: true, underline: true })}`, + profile_url! + )})` : ""; const full_text = `${prefix_emoji} ${name}${username_part}`.trim(); @@ -347,7 +350,9 @@ export class Embed implements EmbedData { container.addTextDisplayComponents((builder) => builder.setContent( - `${stats.length > 0 ? `${subtext(stats.join(" "))}\n` : ""}${embed.emoji} • ${time( + `${stats.length > 0 ? `${subtext(stats.join(" "))}\n` : ""}${ + embed.emoji + } • ${time( embed.timestamp, TimestampStyles.LongDateShortTime )} • ${hyperlink(`View on ${embed.platform}`, embed.url)}` @@ -366,7 +371,9 @@ export class Embed implements EmbedData { return Object.entries(stats_data).map( ([key, val]) => - `${statEmojis[key as keyof StatEmojis]} ${Embed.NumberFormatter.format(val)}` + `${statEmojis[key as keyof StatEmojis]} ${Embed.NumberFormatter.format( + val + )}` ); } } diff --git a/packages/logging/src/main.ts b/packages/logging/src/main.ts index 2b7b550..d421ce8 100644 --- a/packages/logging/src/main.ts +++ b/packages/logging/src/main.ts @@ -29,6 +29,8 @@ export interface EmbedlyInteractionContext { message_id?: string; source?: EmbedlySource; platform?: PlatformName; + error_message?: string; + error_stack?: string; } export const EMBEDLY_NO_LINK_IN_MESSAGE: EmbedlyErrorBase = @@ -56,6 +58,8 @@ export interface EmbedlyPostContext { resp_status?: number; resp_message?: string; resp_data?: any; + error_message?: string; + error_stack?: string; } export const EMBEDLY_FETCH_PLATFORM = ( @@ -180,6 +184,39 @@ export const EMBEDLY_AUTO_DELETE_INFO: EmbedlyLogBase = "Bot embed automatically deleted because the original message was deleted." }; +export const EMBEDLY_UNHANDLED_ERROR: EmbedlyErrorBase = + { + type: "EMBEDLY_UNHANDLED_ERROR", + status: 500, + title: "Unhandled error.", + detail: "An unhandled error occurred while processing a request." + }; + +export const EMBEDLY_CREATE_EMBED_FAILED: EmbedlyErrorBase = + { + type: "EMBEDLY_CREATE_EMBED_FAILED", + status: 500, + title: "Failed to create embed.", + detail: "An error occurred while creating the embed from post data." + }; + +export const EMBEDLY_SEND_MESSAGE_FAILED: EmbedlyErrorBase = + { + type: "EMBEDLY_SEND_MESSAGE_FAILED", + status: 500, + title: "Failed to send message.", + detail: + "An error occurred while sending the embed message to Discord." + }; + +export const EMBEDLY_PARSE_POST_ID_FAILED: EmbedlyErrorBase = + { + type: "EMBEDLY_PARSE_POST_ID_FAILED", + status: 400, + title: "Failed to parse post ID.", + detail: "An error occurred while parsing the post ID from the URL." + }; + export const EMBEDLY_NO_LINK_WARN: EmbedlyLogBase = { type: "EMBEDLY_NO_LINK_WARN", @@ -300,5 +337,7 @@ export class EmbedlyLogger { export function formatDiscord< T extends EmbedlyErrorBase >(err: T, ctx: T["context"]) { - return `**__${err.title}__**\n${err.detail}\n\n-# [${err.type}]: ${ctx?.interaction_id || ctx?.message_id}`; + return `**__${err.title}__**\n${err.detail}\n\n-# [${err.type}]: ${ + ctx?.interaction_id || ctx?.message_id + }`; }