diff --git a/.changeset/ninety-pillows-read.md b/.changeset/ninety-pillows-read.md new file mode 100644 index 0000000..5137b0f --- /dev/null +++ b/.changeset/ninety-pillows-read.md @@ -0,0 +1,5 @@ +--- +"@embedly/bot": minor +--- + +add direct fetch fallback when api fails diff --git a/apps/bot/src/commands/embed.ts b/apps/bot/src/commands/embed.ts index 4150884..ff3916f 100644 --- a/apps/bot/src/commands/embed.ts +++ b/apps/bot/src/commands/embed.ts @@ -1,5 +1,3 @@ -import { treaty } from "@elysiajs/eden"; -import type { App } from "@embedly/api"; import { Embed, EmbedFlagNames, @@ -35,8 +33,7 @@ import { ApplicationIntegrationType, InteractionContextType } from "discord.js"; - -const app = treaty(process.env.EMBEDLY_API_DOMAIN!); +import { fetchPostData } from "../fetch.ts"; export class EmbedCommand extends Command { public constructor( @@ -151,57 +148,24 @@ export class EmbedCommand extends Command { await interaction.deferReply(); - const { data, error } = await this.container.tracer.startActiveSpan( - "fetch_from_api", - async (s) => { - s.setAttribute("embedly.platform", platform.type); - s.setAttribute("embedly.url", url); - - const otelHeaders: Record = {}; - propagation.inject(context.active(), otelHeaders); - - const res = await app.api.scrape.post( - { - platform: platform.type, - url - }, - { - headers: { - authorization: `Bearer ${process.env.DISCORD_BOT_TOKEN}`, - ...otelHeaders - } - } - ); - if (res.error) { - s.setStatus({ - code: SpanStatusCode.ERROR, - message: - "detail" in res.error.value - ? res.error.value.detail - : res.error.value.type - }); - s.recordException( - "detail" in res.error.value - ? res.error.value.detail - : res.error.value.type - ); - } - s.end(); - return res; - } - ); + const otel_headers: Record = {}; + propagation.inject(context.active(), otel_headers); - if (error?.status === 400 || error?.status === 500) { + let data: Record; + try { + data = await fetchPostData(platform.type, url, otel_headers); + } catch (fetch_error: any) { const error_context = { ...log_ctx, platform: platform.type, - ...("context" in error.value ? error.value.context : {}) + error_message: fetch_error.message, + error_stack: fetch_error.stack }; this.container.logger.error( - formatLog(error.value, error_context) + formatLog(EMBEDLY_UNHANDLED_ERROR, error_context) ); return await interaction.editReply({ - content: formatDiscord(error.value, error_context) + content: formatDiscord(EMBEDLY_UNHANDLED_ERROR, error_context) }); } diff --git a/apps/bot/src/fetch.ts b/apps/bot/src/fetch.ts new file mode 100644 index 0000000..644df23 --- /dev/null +++ b/apps/bot/src/fetch.ts @@ -0,0 +1,77 @@ +import { treaty } from "@elysiajs/eden"; +import type { App } from "@embedly/api"; +import Platforms, { + type EmbedlyPlatformType +} from "@embedly/platforms"; +import { SpanStatusCode } from "@opentelemetry/api"; +import { container } from "@sapphire/framework"; + +const app = treaty(process.env.EMBEDLY_API_DOMAIN!); + +export async function fetchPostData( + platform_type: EmbedlyPlatformType, + url: string, + otel_headers: Record +): Promise> { + const handler = Platforms[platform_type]; + + const { data, error } = await container.tracer.startActiveSpan( + "fetch_from_api", + async (s) => { + s.setAttribute("embedly.platform", platform_type); + s.setAttribute("embedly.url", url); + + const res = await app.api.scrape.post( + { platform: platform_type, url }, + { + headers: { + authorization: `Bearer ${process.env.DISCORD_BOT_TOKEN}`, + ...otel_headers + } + } + ); + + if (res.error) { + s.setStatus({ + code: SpanStatusCode.ERROR, + message: + "detail" in res.error.value + ? res.error.value.detail + : res.error.value.type + }); + } + s.end(); + return res; + } + ); + + if (!error) { + return data; + } + + return await container.tracer.startActiveSpan( + "fetch_direct_fallback", + async (s) => { + s.setAttribute("embedly.platform", platform_type); + s.setAttribute("embedly.url", url); + s.setAttribute("embedly.api_error_status", error.status); + + try { + const post_id = await handler.parsePostId(url); + s.setAttribute("embedly.post_id", post_id); + const post_data = await handler.fetchPost(post_id); + s.setStatus({ code: SpanStatusCode.OK }); + return post_data as Record; + } catch (fallback_error: any) { + s.setStatus({ + code: SpanStatusCode.ERROR, + message: fallback_error.message ?? String(fallback_error) + }); + s.recordException(fallback_error); + throw fallback_error; + } finally { + s.end(); + } + } + ); +} diff --git a/apps/bot/src/listeners/messageCreate.ts b/apps/bot/src/listeners/messageCreate.ts index a535a36..0994158 100644 --- a/apps/bot/src/listeners/messageCreate.ts +++ b/apps/bot/src/listeners/messageCreate.ts @@ -1,5 +1,3 @@ -import { treaty } from "@elysiajs/eden"; -import type { App } from "@embedly/api"; import { Embed, EmbedFlagNames, @@ -28,8 +26,7 @@ import { } from "@opentelemetry/api"; import { Events, Listener } from "@sapphire/framework"; import { type Message, MessageFlags } from "discord.js"; - -const app = treaty(process.env.EMBEDLY_API_DOMAIN!); +import { fetchPostData } from "../fetch.ts"; export class MessageListener extends Listener< typeof Events.MessageCreate @@ -84,63 +81,29 @@ export class MessageListener extends Listener< ); if (!platform) continue; - const { data, error } = - await this.container.tracer.startActiveSpan( - "fetch_from_api", - async (s) => { - s.setAttribute("embedly.platform", platform.type); - s.setAttribute("embedly.url", url); - - const otelHeaders: Record = {}; - propagation.inject(context.active(), otelHeaders); + const otel_headers: Record = {}; + propagation.inject(context.active(), otel_headers); - const res = await app.api.scrape.post( - { - platform: platform.type, - url - }, - { - headers: { - authorization: `Bearer ${process.env.DISCORD_BOT_TOKEN}`, - ...otelHeaders - } - } - ); - if (res.error) { - s.setStatus({ - code: SpanStatusCode.ERROR, - message: - "detail" in res.error.value - ? res.error.value.detail - : res.error.value.type - }); - s.recordException( - "detail" in res.error.value - ? res.error.value.detail - : res.error.value.type - ); - } - s.end(); - return res; - } + let data: Record; + try { + data = await fetchPostData( + platform.type, + url, + otel_headers + ); + } catch (fetch_error: any) { + const error_context: EmbedlyInteractionContext & + EmbedlyPostContext = { + message_id: message.id, + user_id: message.author.id, + source: "message", + platform: platform.type, + error_message: fetch_error.message, + error_stack: fetch_error.stack + }; + this.container.logger.error( + formatLog(EMBEDLY_UNHANDLED_ERROR, error_context) ); - - if (error) { - if ("detail" in error.value) { - const error_context: EmbedlyInteractionContext & - EmbedlyPostContext = { - ...("context" in error.value - ? error.value.context - : {}), - message_id: message.id, - user_id: message.author.id, - source: "message", - platform: platform.type - }; - this.container.logger.error( - formatLog(error.value, error_context) - ); - } return; }