From f908f4dd593f2dfee2f31f9d277b7bad718d2326 Mon Sep 17 00:00:00 2001 From: ItsRauf <31735267+ItsRauf@users.noreply.github.com> Date: Thu, 22 Jan 2026 16:55:47 -0600 Subject: [PATCH 01/20] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20feat(platforms):=20c?= =?UTF-8?q?leaned=20up=20code?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit refactored a bunch of the split code between parsing and types to localize it to platforms --- .changeset/config.json | 4 +- .changeset/nice-pans-know.md | 10 ++ .gitignore | 2 + apps/api/package.json | 2 - apps/api/src/main.ts | 7 +- apps/bot/package.json | 1 - apps/bot/src/commands/embed.ts | 5 +- apps/bot/src/listeners/messageCreate.ts | 9 +- packages/builder/package.json | 4 +- packages/builder/src/Embed.ts | 73 +++++++-- packages/logging/package.json | 1 - packages/logging/src/main.ts | 35 ++-- packages/parser/CHANGELOG.md | 209 ------------------------ packages/parser/biome.json | 5 - packages/parser/package.json | 28 ---- packages/parser/src/main.ts | 2 - packages/parser/src/parse.ts | 45 ----- packages/parser/src/regex.ts | 15 -- packages/parser/tsconfig.json | 9 - packages/platforms/package.json | 2 - packages/platforms/src/CBC.ts | 31 ++-- packages/platforms/src/Instagram.ts | 40 ++--- packages/platforms/src/Platform.ts | 71 ++++++-- packages/platforms/src/Reddit.ts | 54 +++--- packages/platforms/src/Threads.ts | 45 +++-- packages/platforms/src/TikTok.ts | 44 ++--- packages/platforms/src/Twitter.ts | 32 ++-- packages/platforms/src/constants.ts | 11 ++ packages/platforms/src/main.ts | 48 +++++- packages/platforms/src/parsing.ts | 42 +++++ packages/platforms/src/types.ts | 43 +++++ packages/types/CHANGELOG.md | 117 ------------- packages/types/biome.json | 5 - packages/types/package.json | 28 ---- packages/types/src/main.ts | 70 -------- packages/types/tsconfig.json | 9 - pnpm-lock.yaml | 62 ------- 37 files changed, 406 insertions(+), 814 deletions(-) create mode 100644 .changeset/nice-pans-know.md delete mode 100644 packages/parser/CHANGELOG.md delete mode 100644 packages/parser/biome.json delete mode 100644 packages/parser/package.json delete mode 100644 packages/parser/src/main.ts delete mode 100644 packages/parser/src/parse.ts delete mode 100644 packages/parser/src/regex.ts delete mode 100644 packages/parser/tsconfig.json create mode 100644 packages/platforms/src/constants.ts create mode 100644 packages/platforms/src/parsing.ts create mode 100644 packages/platforms/src/types.ts delete mode 100644 packages/types/CHANGELOG.md delete mode 100644 packages/types/biome.json delete mode 100644 packages/types/package.json delete mode 100644 packages/types/src/main.ts delete mode 100644 packages/types/tsconfig.json diff --git a/.changeset/config.json b/.changeset/config.json index 479cbd0..6a096ec 100644 --- a/.changeset/config.json +++ b/.changeset/config.json @@ -14,9 +14,7 @@ "@embedly/builder", "@embedly/config", "@embedly/logging", - "@embedly/parser", - "@embedly/platforms", - "@embedly/types" + "@embedly/platforms" ] ], "linked": [], diff --git a/.changeset/nice-pans-know.md b/.changeset/nice-pans-know.md new file mode 100644 index 0000000..75a33f6 --- /dev/null +++ b/.changeset/nice-pans-know.md @@ -0,0 +1,10 @@ +--- +"@embedly/platforms": minor +"@embedly/builder": minor +"@embedly/logging": minor +"@embedly/config": minor +"@embedly/api": minor +"@embedly/bot": minor +--- + +refactored a bunch of the code surrounding types and parsing diff --git a/.gitignore b/.gitignore index 55e77a0..ecf9e98 100644 --- a/.gitignore +++ b/.gitignore @@ -38,3 +38,5 @@ yarn.lock .env AGENTS.md +.claude +.serena diff --git a/apps/api/package.json b/apps/api/package.json index 72689d7..3b2a3eb 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -24,9 +24,7 @@ "@discordjs/rest": "^2.6.0", "@embedly/builder": "workspace:*", "@embedly/logging": "workspace:*", - "@embedly/parser": "workspace:*", "@embedly/platforms": "workspace:*", - "@embedly/types": "workspace:*", "@logtail/edge": "^0.5.7", "@logtail/node": "^0.5.6", "discord-verify": "^1.2.0", diff --git a/apps/api/src/main.ts b/apps/api/src/main.ts index 2b3f073..cb53f4a 100644 --- a/apps/api/src/main.ts +++ b/apps/api/src/main.ts @@ -6,13 +6,12 @@ import { type EmbedlyPostContext, formatBetterStack } from "@embedly/logging"; -import { +import Platforms, { + EmbedlyPlatformType, GENERIC_LINK_REGEX, getPlatformFromURL, hasLink -} from "@embedly/parser"; -import Platforms from "@embedly/platforms"; -import { EmbedlyPlatformType } from "@embedly/types"; +} from "@embedly/platforms"; import { Logtail } from "@logtail/edge"; import { Elysia, t } from "elysia"; diff --git a/apps/bot/package.json b/apps/bot/package.json index ba7a728..aec3d28 100644 --- a/apps/bot/package.json +++ b/apps/bot/package.json @@ -23,7 +23,6 @@ "@embedly/builder": "workspace:*", "@embedly/config": "workspace:*", "@embedly/logging": "workspace:*", - "@embedly/parser": "workspace:*", "@embedly/platforms": "workspace:*", "@types/node": "^25.0.3", "discord-api-types": "^0.38.37", diff --git a/apps/bot/src/commands/embed.ts b/apps/bot/src/commands/embed.ts index e36f651..5c6ea29 100644 --- a/apps/bot/src/commands/embed.ts +++ b/apps/bot/src/commands/embed.ts @@ -15,12 +15,11 @@ import { formatBetterStack, formatDiscord } from "@embedly/logging"; -import { +import Platforms, { GENERIC_LINK_REGEX, getPlatformFromURL, hasLink -} from "@embedly/parser"; -import Platforms from "@embedly/platforms"; +} from "@embedly/platforms"; import { Command } from "@sapphire/framework"; import { ApplicationCommandType, diff --git a/apps/bot/src/listeners/messageCreate.ts b/apps/bot/src/listeners/messageCreate.ts index 218a28e..308d395 100644 --- a/apps/bot/src/listeners/messageCreate.ts +++ b/apps/bot/src/listeners/messageCreate.ts @@ -11,14 +11,13 @@ import { type EmbedlyPostContext, formatBetterStack } from "@embedly/logging"; -import { +import Platforms, { GENERIC_LINK_REGEX, getPlatformFromURL, hasLink, isEscaped, isSpoiler -} from "@embedly/parser"; -import Platforms from "@embedly/platforms"; +} from "@embedly/platforms"; import { Events, Listener } from "@sapphire/framework"; import { type Message, MessageFlags } from "discord.js"; @@ -49,9 +48,9 @@ export class MessageListener extends Listener< ); if (!urls) return; for (const [ind, url] of urls.entries()) { - if (isEscaped(url, message.content)) return; + if (isEscaped(url, message.content)) continue; const platform = getPlatformFromURL(url); - if (!platform) return; + if (!platform) continue; const { data, error } = await app.api.scrape.post( { diff --git a/packages/builder/package.json b/packages/builder/package.json index f73dbb3..100d9ba 100644 --- a/packages/builder/package.json +++ b/packages/builder/package.json @@ -14,7 +14,7 @@ }, "scripts": { "lint": "biome ci ./src/*", - "build": "pkgroll --sourcemap=inline && tsc && tsc" + "build": "pkgroll --sourcemap=inline && tsc" }, "devDependencies": { "@biomejs/biome": "2.3.10", @@ -24,8 +24,6 @@ }, "dependencies": { "@discordjs/builders": "^1.13.1", - "@embedly/parser": "workspace:*", - "@embedly/types": "workspace:*", "discord-api-types": "^0.38.37" } } diff --git a/packages/builder/src/Embed.ts b/packages/builder/src/Embed.ts index 89a0041..8f6d793 100644 --- a/packages/builder/src/Embed.ts +++ b/packages/builder/src/Embed.ts @@ -6,26 +6,64 @@ import { heading, hyperlink, MediaGalleryBuilder, + type RGBTuple, SectionBuilder, subtext, TimestampStyles, time } from "@discordjs/builders"; -import { - type BaseEmbedData, - type BaseEmbedDataWithoutPlatform, - EmbedlyPlatformColors, - type EmbedlyPlatformType, - type Emojis, - emojis, - type StatsData -} from "@embedly/types"; + import { type APIMediaGalleryItem, ButtonStyle, SeparatorSpacingSize } from "discord-api-types/v10"; +// ============================================================================ +// Types - moved from @embedly/types +// ============================================================================ + +export interface StatsData { + comments: number; + reposts?: number; + likes: number; + bookmarks?: number; + views?: number; +} + +// Stat emojis (not platform-specific) +export const statEmojis = { + comments: "<:comment:1386639521373753374>", + reposts: "<:repost:1386639564143198349>", + likes: "<:like:1386639662772391987>", + bookmarks: "<:bookmark:1386639640433529014>", + views: "<:view:1386639685237084292>", + reply: "<:reply:1386639619768058007>", + quote: "<:quote:1389657738480713838>" +} as const; + +export type StatEmojis = typeof statEmojis; + +export interface BaseEmbedData { + platform: string; + color: RGBTuple; + emoji: string; + name: string; + username?: string; + profile_url?: string; + avatar_url: string; + url: string; + timestamp: number; + stats?: StatsData; + description?: string; + media?: APIMediaGalleryItem[]; +} + +export type BaseEmbedDataWithoutPlatform = Omit< + BaseEmbedData, + "platform" | "color" | "emoji" +>; + export interface EmbedData extends BaseEmbedData { quote?: BaseEmbedDataWithoutPlatform; replying_to?: BaseEmbedDataWithoutPlatform; @@ -46,7 +84,9 @@ export interface EmbedFlags { } export class Embed implements EmbedData { - public platform!: EmbedlyPlatformType; + public platform!: string; + public color!: RGBTuple; + public emoji!: string; public name!: string; public username?: string; public profile_url?: string; @@ -97,7 +137,7 @@ export class Embed implements EmbedData { } const container = new ContainerBuilder(); - container.setAccentColor(EmbedlyPlatformColors[embed.platform]); + container.setAccentColor(embed.color); if (hidden) { container.setSpoiler(true); @@ -152,7 +192,8 @@ export class Embed implements EmbedData { author_profile_url = embed.profile_url; author_description = embed.description; author_avatar_url = embed.avatar_url; - prefix_emoji = !source_only && embed.quote ? emojis.quote : ""; + prefix_emoji = + !source_only && embed.quote ? statEmojis.quote : ""; } else { // Show reply content first (only when not source_only) author_name = embed.replying_to.name; @@ -230,7 +271,7 @@ export class Embed implements EmbedData { embed.profile_url, embed.avatar_url, embed.description, - emojis.reply + statEmojis.reply ); container.addSectionComponents(reply_section); @@ -319,7 +360,7 @@ export class Embed implements EmbedData { container.addSectionComponents((builder) => { builder.addTextDisplayComponents((builder) => builder.setContent( - `${stats.length > 0 ? `${subtext(stats.join(" "))}\n` : ""}${emojis[embed.platform]} • ${time( + `${stats.length > 0 ? `${subtext(stats.join(" "))}\n` : ""}${embed.emoji} • ${time( embed.timestamp, TimestampStyles.LongDateShortTime )}` @@ -344,7 +385,7 @@ export class Embed implements EmbedData { container.addTextDisplayComponents((builder) => builder.setContent( - `${stats.length > 0 ? `${subtext(stats.join(" "))}\n` : ""}${emojis[embed.platform]} • ${time( + `${stats.length > 0 ? `${subtext(stats.join(" "))}\n` : ""}${embed.emoji} • ${time( embed.timestamp, TimestampStyles.LongDateShortTime )} • ${link_style === "inline" ? hyperlink(`View on ${embed.platform}`, embed.url) : ""}` @@ -363,7 +404,7 @@ export class Embed implements EmbedData { return Object.entries(stats_data).map( ([key, val]) => - `${emojis[key as keyof Emojis]} ${Embed.NumberFormatter.format(val)}` + `${statEmojis[key as keyof StatEmojis]} ${Embed.NumberFormatter.format(val)}` ); } } diff --git a/packages/logging/package.json b/packages/logging/package.json index 8528fd4..a4dec7d 100644 --- a/packages/logging/package.json +++ b/packages/logging/package.json @@ -19,7 +19,6 @@ "devDependencies": { "@biomejs/biome": "2.3.10", "@embedly/config": "workspace:*", - "@embedly/types": "workspace:*", "pkgroll": "^2.21.4", "typescript": "^5.9.3" } diff --git a/packages/logging/src/main.ts b/packages/logging/src/main.ts index 50968fd..948b9c0 100644 --- a/packages/logging/src/main.ts +++ b/packages/logging/src/main.ts @@ -1,4 +1,6 @@ -import type { EmbedlyPlatformType } from "@embedly/types"; +// Platform type is just a string for logging purposes +// This avoids circular dependency with @embedly/types +type PlatformName = string; export interface EmbedlyLogBase { type: string; @@ -46,7 +48,7 @@ export const EMBEDLY_NO_VALID_LINK: EmbedlyErrorBase }; export interface EmbedlyPostContext { - platform?: EmbedlyPlatformType; + platform?: PlatformName; post_url?: string; post_id?: string; resp_status?: number; @@ -54,25 +56,24 @@ export interface EmbedlyPostContext { resp_data?: any; } -export const EMBEDLY_FETCH_PLATFORM: ( - platform: EmbedlyPlatformType -) => EmbedlyErrorBase = ( - platform: EmbedlyPlatformType -) => ({ +export const EMBEDLY_FETCH_PLATFORM = ( + platform: PlatformName +): EmbedlyErrorBase => ({ type: `EMBEDLY_FETCH_${platform.toUpperCase()}`, title: `Fetching ${platform}.`, detail: `Fetching ${platform} from the ${platform} API.` }); -export const EMBEDLY_FAILED_PLATFORM: ( - platform: EmbedlyPlatformType -) => EmbedlyErrorBase = - (platform: EmbedlyPlatformType) => ({ - type: `EMBEDLY_FAILED_${platform.toUpperCase()}`, - status: 500, - title: `Failed to fetch ${platform}.`, - detail: `Failed to fetch this ${platform} post from the ${platform} API.` - }); +export const EMBEDLY_FAILED_PLATFORM = ( + platform: PlatformName +): EmbedlyErrorBase< + EmbedlyInteractionContext & EmbedlyPostContext +> => ({ + type: `EMBEDLY_FAILED_${platform.toUpperCase()}`, + status: 500, + title: `Failed to fetch ${platform}.`, + detail: `Failed to fetch this ${platform} post from the ${platform} API.` +}); export const EMBEDLY_CACHED_POST: EmbedlyErrorBase = { @@ -127,7 +128,7 @@ export const EMBEDLY_DELETE_SUCCESS: EmbedlyErrorBase }; export interface EmbedlyEmbedContext extends EmbedlyInteractionContext { - platform?: EmbedlyPlatformType; + platform?: PlatformName; url?: string; bot_message_id?: string; user_message_id?: string; diff --git a/packages/parser/CHANGELOG.md b/packages/parser/CHANGELOG.md deleted file mode 100644 index 5dc84be..0000000 --- a/packages/parser/CHANGELOG.md +++ /dev/null @@ -1,209 +0,0 @@ -# @embedly/parser - -## 0.11.0 - -### Patch Changes - -- Updated dependencies []: - - @embedly/types@0.11.0 - -## 0.10.0 - -### Patch Changes - -- [#36](https://github.com/embed-team/embedly/pull/36) [`dde5d23`](https://github.com/embed-team/embedly/commit/dde5d23d2d3705dc743fb772d602f64d85069f8c) Thanks [@ItsRauf](https://github.com/ItsRauf)! - feat(ci): split github actions across multiple files - -- Updated dependencies [[`dde5d23`](https://github.com/embed-team/embedly/commit/dde5d23d2d3705dc743fb772d602f64d85069f8c)]: - - @embedly/types@0.10.0 - -## 0.9.1 - -### Patch Changes - -- [#34](https://github.com/embed-team/embedly/pull/34) [`d8495b4`](https://github.com/embed-team/embedly/commit/d8495b40953bce13cb528dfb28f467cdbf19d07c) Thanks [@ItsRauf](https://github.com/ItsRauf)! - feat(ci): split github actions across multiple files - -- Updated dependencies [[`d8495b4`](https://github.com/embed-team/embedly/commit/d8495b40953bce13cb528dfb28f467cdbf19d07c)]: - - @embedly/types@0.9.1 - -## 0.9.0 - -### Patch Changes - -- Updated dependencies []: - - @embedly/types@0.9.0 - -## 0.8.0 - -### Patch Changes - -- [#29](https://github.com/embed-team/embedly/pull/29) [`9c5c80d`](https://github.com/embed-team/embedly/commit/9c5c80d3015bcee08027856d919283f8a7cd916c) Thanks [@ItsRauf](https://github.com/ItsRauf)! - cbc regex was only capturing a subset of links - -- Updated dependencies []: - - @embedly/types@0.8.0 - -## 0.7.1 - -### Patch Changes - -- Updated dependencies []: - - @embedly/types@0.7.1 - -## 0.7.0 - -### Patch Changes - -- Updated dependencies []: - - @embedly/types@0.7.0 - -## 0.6.0 - -### Minor Changes - -- [#22](https://github.com/embed-team/embedly/pull/22) [`327acfe`](https://github.com/embed-team/embedly/commit/327acfe0c405fd57b65643e881bd4978af47e0bb) Thanks [@ItsRauf](https://github.com/ItsRauf)! - refactor embed builder and fix delete command - -### Patch Changes - -- [#22](https://github.com/embed-team/embedly/pull/22) [`327acfe`](https://github.com/embed-team/embedly/commit/327acfe0c405fd57b65643e881bd4978af47e0bb) Thanks [@ItsRauf](https://github.com/ItsRauf)! - replies now show in the correct order - -- Updated dependencies [[`327acfe`](https://github.com/embed-team/embedly/commit/327acfe0c405fd57b65643e881bd4978af47e0bb), [`327acfe`](https://github.com/embed-team/embedly/commit/327acfe0c405fd57b65643e881bd4978af47e0bb)]: - - @embedly/types@0.6.0 - -## 0.5.3 - -### Patch Changes - -- [#20](https://github.com/embed-team/embedly/pull/20) [`455d9b1`](https://github.com/embed-team/embedly/commit/455d9b19b0c8662a85799557fedd8b9bfae2cc33) Thanks [@ItsRauf](https://github.com/ItsRauf)! - sane spoiler and escape detection - -- Updated dependencies [[`455d9b1`](https://github.com/embed-team/embedly/commit/455d9b19b0c8662a85799557fedd8b9bfae2cc33)]: - - @embedly/types@0.5.3 - -## 0.5.2 - -### Patch Changes - -- Updated dependencies []: - - @embedly/types@0.5.2 - -## 0.5.1 - -### Patch Changes - -- Updated dependencies []: - - @embedly/types@0.5.1 - -## 0.5.0 - -### Patch Changes - -- Updated dependencies []: - - @embedly/types@0.5.0 - -## 0.4.1 - -### Patch Changes - -- [#8](https://github.com/embed-team/embedly/pull/8) [`4386504`](https://github.com/embed-team/embedly/commit/438650487368ec722bc75801ccf5f495be62a485) Thanks [@ItsRauf](https://github.com/ItsRauf)! - fix broken spoiler regex - -- Updated dependencies [[`4386504`](https://github.com/embed-team/embedly/commit/438650487368ec722bc75801ccf5f495be62a485)]: - - @embedly/types@0.4.1 - -## 0.4.0 - -### Minor Changes - -- [#6](https://github.com/embed-team/embedly/pull/6) [`14785b7`](https://github.com/embed-team/embedly/commit/14785b70759445b7d402a3d63bca72993239f5b3) Thanks [@ItsRauf](https://github.com/ItsRauf)! - add a spoiler flag and support masked links in messages - -### Patch Changes - -- Updated dependencies [[`14785b7`](https://github.com/embed-team/embedly/commit/14785b70759445b7d402a3d63bca72993239f5b3)]: - - @embedly/types@0.4.0 - -## 0.3.3 - -### Patch Changes - -- [`6d3fccd`](https://github.com/embed-team/embedly/commit/6d3fccd8190a8b697e0ee93edc8c81affb036f01) Thanks [@ItsRauf](https://github.com/ItsRauf)! - added DX and CI tooling - -- Updated dependencies [[`6d3fccd`](https://github.com/embed-team/embedly/commit/6d3fccd8190a8b697e0ee93edc8c81affb036f01)]: - - @embedly/types@0.3.3 - -## 0.3.2 - -### Patch Changes - -- fixed title for cbc embeds where username doesn't exist -- Updated dependencies - - @embedly/types@0.3.2 - -## 0.3.1 - -### Patch Changes - -- escape markdown in embed description -- Updated dependencies - - @embedly/types@0.3.1 - -## 0.3.0 - -### Minor Changes - -- added threads and fixed a few emoji issues - -### Patch Changes - -- Updated dependencies - - @embedly/types@0.3.0 - -## 0.2.0 - -### Minor Changes - -- added cbc.ca, fixed tiktok, added media proxy - -### Patch Changes - -- Updated dependencies - - @embedly/types@0.2.0 - -## 0.1.4 - -### Patch Changes - -- added a fallback error for random failures and added multi-embedding for messages -- Updated dependencies - - @embedly/types@0.1.4 - -## 0.1.3 - -### Patch Changes - -- fixed tiktok embeds and added new instagram share link handling -- Updated dependencies - - @embedly/types@0.1.3 - -## 0.1.2 - -### Patch Changes - -- rewrote api and bot embed handling to make the api only handle scraping -- Updated dependencies - - @embedly/types@0.1.2 - -## 0.1.1 - -### Patch Changes - -- added sidebar color to containers -- Updated dependencies - - @embedly/types@0.1.1 - -## 0.1.0 - -### Minor Changes - -- this is the first oss version of embedly :3 - -### Patch Changes - -- Updated dependencies - - @embedly/types@0.1.0 diff --git a/packages/parser/biome.json b/packages/parser/biome.json deleted file mode 100644 index 167b18f..0000000 --- a/packages/parser/biome.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "root": false, - "$schema": "./node_modules/@biomejs/biome/configuration_schema.json", - "extends": ["@embedly/config/biome.json"] -} diff --git a/packages/parser/package.json b/packages/parser/package.json deleted file mode 100644 index 088b424..0000000 --- a/packages/parser/package.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "name": "@embedly/parser", - "version": "0.11.0", - "private": true, - "type": "module", - "main": "./dist/main.js", - "module": "./dist/main.js", - "types": "./dist/main.d.ts", - "exports": { - "import": { - "types": "./dist/main.d.ts", - "default": "./dist/main.js" - } - }, - "scripts": { - "lint": "biome ci ./src/*", - "build": "pkgroll --sourcemap=inline && tsc" - }, - "devDependencies": { - "@biomejs/biome": "2.3.10", - "@embedly/config": "workspace:*", - "pkgroll": "^2.21.4", - "typescript": "^5.9.3" - }, - "dependencies": { - "@embedly/types": "workspace:*" - } -} diff --git a/packages/parser/src/main.ts b/packages/parser/src/main.ts deleted file mode 100644 index 1459049..0000000 --- a/packages/parser/src/main.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from "./parse.ts"; -export * from "./regex.ts"; diff --git a/packages/parser/src/parse.ts b/packages/parser/src/parse.ts deleted file mode 100644 index 7cfe006..0000000 --- a/packages/parser/src/parse.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { EmbedlyPlatformType } from "@embedly/types"; -import { - CBC_REGEX, - GENERIC_LINK_REGEX, - IG_REGEX, - REDDIT_REGEX, - THREADS_REGEX, - TIKTOK_REGEX_MAIN, - TWITTER_REGEX -} from "./regex.ts"; - -export function hasLink(content: string) { - return GENERIC_LINK_REGEX.test(content); -} - -export function isSpoiler(url: string, content: string) { - return content - .split("||") - .some((part, ind) => ind % 2 === 1 && part.includes(url)); -} - -export function isEscaped(url: string, content: string) { - return content.includes(`<${url}>`); -} - -export function getPlatformFromURL( - url: string -): null | { type: EmbedlyPlatformType } { - if (TWITTER_REGEX.test(url)) - return { type: EmbedlyPlatformType.Twitter }; - if (IG_REGEX.test(url)) - return { type: EmbedlyPlatformType.Instagram }; - if (TIKTOK_REGEX_MAIN.test(url)) - return { type: EmbedlyPlatformType.TikTok }; - if (CBC_REGEX.test(url)) { - return { type: EmbedlyPlatformType.CBC }; - } - if (THREADS_REGEX.test(url)) { - return { type: EmbedlyPlatformType.Threads }; - } - if (REDDIT_REGEX.test(url)) { - return { type: EmbedlyPlatformType.Reddit }; - } - return null; -} diff --git a/packages/parser/src/regex.ts b/packages/parser/src/regex.ts deleted file mode 100644 index 7688a73..0000000 --- a/packages/parser/src/regex.ts +++ /dev/null @@ -1,15 +0,0 @@ -export const GENERIC_LINK_REGEX = - /\b((?:[a-z][\w-]+:(?:\/{1,3}|[a-z0-9%])|www\d{0,3}[.]|[a-z0-9.-]+[.][a-z]{2,4}\/)(?:[^\s()<>]+|\(([^\s()<>]+|(\([^\s()<>]+\)))*\))+(?:\(([^\s()<>]+|(\([^\s()<>]+\)))*\)|[^\s`!()[\]{};:'".,<>?«»“”‘’]))/i; -export const TWITTER_REGEX = - /(?:twitter|x).com\/.*\/status(?:es)?\/(?[^/?]+)/; -export const IG_REGEX = - /instagram.com\/(?:[A-Za-z0-9_.]+\/)?(p|share|reels|reel|stories)\/(?[A-Za-z0-9-_]+)/; -export const TIKTOK_REGEX_MAIN = - /(https?:\/\/)?(?:[\w-]+\.)*tiktok\.com/; -export const TIKTOK_REGEX = - /https:\/\/(?:m|www|vm)?\.?tiktok\.com\/(?@[\w.-]+)\/video\/(?\d+)/; -export const THREADS_REGEX = - /threads\.com\/@.*\/post\/(?[A-Za-z0-9-_]+)/; -export const CBC_REGEX = /cbc.ca\/.*(?\d\.\d+)/; -export const REDDIT_REGEX = - /https?:\/\/(?:www\.|old\.|m\.)?reddit\.com\/r\/(?\w+)\/comments\/(?[a-z0-9]+)/; diff --git a/packages/parser/tsconfig.json b/packages/parser/tsconfig.json deleted file mode 100644 index 3a138a9..0000000 --- a/packages/parser/tsconfig.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "extends": "@embedly/config/tsconfig.json", - "compilerOptions": { - "lib": ["WebWorker", "ESNext"], - "outDir": "./dist" - }, - "exclude": ["node_modules"], - "include": ["src"] -} diff --git a/packages/platforms/package.json b/packages/platforms/package.json index 3a7eead..9fa3b67 100644 --- a/packages/platforms/package.json +++ b/packages/platforms/package.json @@ -27,8 +27,6 @@ "dependencies": { "@embedly/builder": "workspace:*", "@embedly/logging": "workspace:*", - "@embedly/parser": "workspace:*", - "@embedly/types": "workspace:*", "@types/he": "^1.2.3", "cheerio": "^1.1.2", "he": "^1.2.0" diff --git a/packages/platforms/src/CBC.ts b/packages/platforms/src/CBC.ts index 5acbb68..feab2dd 100644 --- a/packages/platforms/src/CBC.ts +++ b/packages/platforms/src/CBC.ts @@ -1,28 +1,22 @@ import { createHmac } from "node:crypto"; import { Embed } from "@embedly/builder"; -import { - EMBEDLY_FAILED_PLATFORM, - EMBEDLY_FETCH_PLATFORM -} from "@embedly/logging"; -import { CBC_REGEX } from "@embedly/parser"; -import { - type BaseEmbedData, - EmbedlyPlatformType -} from "@embedly/types"; import * as cheerio from "cheerio"; import he from "he"; -import { EmbedlyPlatform } from "./Platform.ts"; +import { CF_CACHE_OPTIONS } from "./constants.ts"; +import { type BaseEmbedData, EmbedlyPlatform } from "./Platform.ts"; +import { EmbedlyPlatformType } from "./types.ts"; export class CBC extends EmbedlyPlatform { + readonly color = [215, 36, 42] as const; + readonly emoji = "<:cbc:1409997044495683674>"; + readonly regex = /cbc.ca\/.*(?\d\.\d+)/; + constructor() { - super(EmbedlyPlatformType.CBC, "cbc.ca", { - fetching: EMBEDLY_FETCH_PLATFORM(EmbedlyPlatformType.CBC), - failed: EMBEDLY_FAILED_PLATFORM(EmbedlyPlatformType.CBC) - }); + super(EmbedlyPlatformType.CBC, "cbc.ca"); } async parsePostId(url: string): Promise { - const match = CBC_REGEX.exec(url)!; + const match = this.regex.exec(url)!; const { cbc_id } = match.groups!; return cbc_id; } @@ -31,10 +25,7 @@ export class CBC extends EmbedlyPlatform { const resp = await fetch(`https://cbc.ca/${post_id}`, { method: "GET", redirect: "follow", - cf: { - cacheTtl: 60 * 60 * 24, - cacheEverything: true - } + ...CF_CACHE_OPTIONS }); if (!resp.ok) { throw { code: resp.status, message: resp.statusText }; @@ -54,6 +45,8 @@ export class CBC extends EmbedlyPlatform { transformRawData(raw_data: any): BaseEmbedData { return { platform: this.name, + color: [...this.color], + emoji: this.emoji, name: he.decode(raw_data.name), avatar_url: this.signProxyURL(raw_data.publisher.logo), timestamp: Math.floor( diff --git a/packages/platforms/src/Instagram.ts b/packages/platforms/src/Instagram.ts index bae7742..a767ab3 100644 --- a/packages/platforms/src/Instagram.ts +++ b/packages/platforms/src/Instagram.ts @@ -1,21 +1,20 @@ import { Embed } from "@embedly/builder"; -import { - EMBEDLY_FAILED_PLATFORM, - EMBEDLY_FETCH_PLATFORM -} from "@embedly/logging"; -import { IG_REGEX } from "@embedly/parser"; +import { CF_CACHE_OPTIONS } from "./constants.ts"; import { type BaseEmbedData, - EmbedlyPlatformType -} from "@embedly/types"; -import { EmbedlyPlatform } from "./Platform.ts"; + type CloudflareEnv, + EmbedlyPlatform +} from "./Platform.ts"; +import { EmbedlyPlatformType } from "./types.ts"; export class Instagram extends EmbedlyPlatform { + readonly color = [225, 48, 108] as const; + readonly emoji = "<:instagram:1386639712013254748>"; + readonly regex = + /instagram.com\/(?:[A-Za-z0-9_.]+\/)?(p|share|reels|reel|stories)\/(?[A-Za-z0-9-_]+)/; + constructor() { - super(EmbedlyPlatformType.Instagram, "insta", { - fetching: EMBEDLY_FETCH_PLATFORM(EmbedlyPlatformType.Instagram), - failed: EMBEDLY_FAILED_PLATFORM(EmbedlyPlatformType.Instagram) - }); + super(EmbedlyPlatformType.Instagram, "insta"); } async parsePostId(url: string): Promise { @@ -28,14 +27,14 @@ export class Instagram extends EmbedlyPlatform { }); url = req.url; } - const match = IG_REGEX.exec(url)!; + const match = this.regex.exec(url)!; const { ig_shortcode } = match.groups!; return ig_shortcode; } async fetchPost( ig_shortcode: string, - env: { EMBED_USER_AGENT: string } + env?: Partial ): Promise { const graphql = new URL(`https://www.instagram.com/api/graphql`); graphql.searchParams.set( @@ -48,17 +47,14 @@ export class Instagram extends EmbedlyPlatform { const resp = await fetch(graphql.toString(), { method: "POST", headers: { - "User-Agent": env.EMBED_USER_AGENT, + "User-Agent": env?.EMBED_USER_AGENT ?? "", "Content-Type": "application/x-www-form-urlencoded", "X-IG-App-ID": "936619743392459", "X-FB-LSD": "AVqbxe3J_YA", "X-ASBD-ID": "129477", "Sec-Fetch-Site": "same-origin" }, - cf: { - cacheTtl: 60 * 60 * 24, - cacheEverything: true - } + ...CF_CACHE_OPTIONS }); if (!resp.ok) { throw { code: resp.status, message: resp.statusText }; @@ -112,6 +108,8 @@ export class Instagram extends EmbedlyPlatform { transformRawData(raw_data: any): BaseEmbedData { return { platform: this.name, + color: [...this.color], + emoji: this.emoji, name: raw_data.owner.full_name, username: raw_data.owner.username, profile_url: `https://instagram.com/${raw_data.owner.username}`, @@ -130,9 +128,7 @@ export class Instagram extends EmbedlyPlatform { createEmbed(post_data: any): Embed { const embed = new Embed(this.transformRawData(post_data)); const media = this.parsePostMedia(post_data); - if (media.length > 10) { - media.length = 10; - } + // Media truncation will be handled by Embed.setMedia embed.setMedia(media); return embed; diff --git a/packages/platforms/src/Platform.ts b/packages/platforms/src/Platform.ts index 5edae6c..51a083d 100644 --- a/packages/platforms/src/Platform.ts +++ b/packages/platforms/src/Platform.ts @@ -1,13 +1,15 @@ -import type { Embed } from "@embedly/builder"; -import type { - EmbedlyErrorBase, - EmbedlyInteractionContext, - EmbedlyPostContext +import type { BaseEmbedData } from "@embedly/builder"; +import { + EMBEDLY_FAILED_PLATFORM, + EMBEDLY_FETCH_PLATFORM, + type EmbedlyErrorBase, + type EmbedlyInteractionContext, + type EmbedlyPostContext } from "@embedly/logging"; -import type { - BaseEmbedData, - EmbedlyPlatformType -} from "@embedly/types"; +import type { EmbedlyPlatformType } from "./types.ts"; + +// Re-export for convenience +export type { BaseEmbedData } from "@embedly/builder"; export interface EmbedlyPlatformLogMessages { fetching: EmbedlyErrorBase; @@ -15,37 +17,74 @@ export interface EmbedlyPlatformLogMessages { EmbedlyInteractionContext & EmbedlyPostContext >; } + +export interface CloudflareEnv { + EMBED_USER_AGENT: string; + THREADS_CSRF_TOKEN?: string; + STORAGE: KVNamespace; + BETTERSTACK_SOURCE_TOKEN: string; + BETTERSTACK_INGESTING_HOST: string; + DISCORD_BOT_TOKEN: string; +} + export abstract class EmbedlyPlatform { + // Config properties - each platform must define these + abstract readonly color: readonly [number, number, number]; + abstract readonly emoji: string; + abstract readonly regex: RegExp; + + // Auto-generated log messages + public log_messages: EmbedlyPlatformLogMessages; + constructor( public name: EmbedlyPlatformType, - private cache_prefix: string, - public log_messages: EmbedlyPlatformLogMessages - ) {} + private cache_prefix: string + ) { + // Auto-generate log messages from platform name + this.log_messages = { + fetching: EMBEDLY_FETCH_PLATFORM(name), + failed: EMBEDLY_FAILED_PLATFORM(name) + }; + } + + /** + * Test if a URL matches this platform's regex + */ + public matchesUrl(url: string): boolean { + return this.regex.test(url); + } abstract parsePostId(url: string): Promise; + public async getPostFromCache( post_id: string, cache_store: KVNamespace - ) { + ): Promise | null> { const cache_key = `${this.cache_prefix}:${post_id}`; return await cache_store.get>(cache_key, { cacheTtl: 60 * 60 * 24, type: "json" }); } + public async addPostToCache( post_id: string, post_data: any, cache_store: KVNamespace - ) { + ): Promise { const cache_key = `${this.cache_prefix}:${post_id}`; cache_store.put(cache_key, JSON.stringify(post_data), { expirationTtl: 60 * 60 * 24 }); } - abstract fetchPost(post_id: string, env?: any): Promise; + + // env is optional and partial - each platform uses what it needs + abstract fetchPost( + post_id: string, + env?: Partial + ): Promise; abstract transformRawData( raw_data: any ): Promise | BaseEmbedData; - abstract createEmbed(post_data: T): Promise | Embed; + abstract createEmbed(post_data: T): Promise | any; } diff --git a/packages/platforms/src/Reddit.ts b/packages/platforms/src/Reddit.ts index d4b4d78..a13b8fe 100644 --- a/packages/platforms/src/Reddit.ts +++ b/packages/platforms/src/Reddit.ts @@ -1,32 +1,31 @@ import { Embed } from "@embedly/builder"; -import { - EMBEDLY_FAILED_PLATFORM, - EMBEDLY_FETCH_PLATFORM -} from "@embedly/logging"; -import { REDDIT_REGEX } from "@embedly/parser"; +import { CF_CACHE_OPTIONS } from "./constants.ts"; import { type BaseEmbedData, - EmbedlyPlatformType -} from "@embedly/types"; -import { EmbedlyPlatform } from "./Platform.ts"; + type CloudflareEnv, + EmbedlyPlatform +} from "./Platform.ts"; +import { EmbedlyPlatformType } from "./types.ts"; export class Reddit extends EmbedlyPlatform { + readonly color = [255, 86, 0] as const; + readonly emoji = "<:reddit:1461320093240655922>"; + readonly regex = + /https?:\/\/(?:www\.|old\.|m\.)?reddit\.com\/r\/(?\w+)\/comments\/(?[a-z0-9]+)/; + constructor() { - super(EmbedlyPlatformType.Reddit, "reddit", { - fetching: EMBEDLY_FETCH_PLATFORM(EmbedlyPlatformType.Reddit), - failed: EMBEDLY_FAILED_PLATFORM(EmbedlyPlatformType.Reddit) - }); + super(EmbedlyPlatformType.Reddit, "reddit"); } async parsePostId(url: string): Promise { - const match = REDDIT_REGEX.exec(url)!; + const match = this.regex.exec(url)!; const { post_id, subreddit } = match.groups!; return `${subreddit}/${post_id}`; } async fetchPost( post_id: string, - env: { EMBED_USER_AGENT: string } + env?: Partial ): Promise { const [subreddit, reddit_id] = post_id.split("/"); const resp = await fetch( @@ -34,12 +33,9 @@ export class Reddit extends EmbedlyPlatform { { method: "GET", headers: { - "User-Agent": env.EMBED_USER_AGENT + "User-Agent": env?.EMBED_USER_AGENT ?? "" }, - cf: { - cacheTtl: 60 * 60 * 24, - cacheEverything: true - } + ...CF_CACHE_OPTIONS } ); @@ -50,21 +46,21 @@ export class Reddit extends EmbedlyPlatform { const post_data = (await resp.json()) as Record; const profile_resp = await fetch( - `https://www.reddit.com/user/${post_data.author}/about.json?raw_json=1`, + `https://www.reddit.com/user/${post_data[0].data.children[0].data.author}/about.json?raw_json=1`, { method: "GET", headers: { - "User-Agent": env.EMBED_USER_AGENT + "User-Agent": env?.EMBED_USER_AGENT ?? "" }, - cf: { - cacheTtl: 60 * 60 * 24, - cacheEverything: true - } + ...CF_CACHE_OPTIONS } ); if (!profile_resp.ok) { - throw { code: resp.status, message: resp.statusText }; + throw { + code: profile_resp.status, + message: profile_resp.statusText + }; } const { data: profile_data } = @@ -125,6 +121,8 @@ export class Reddit extends EmbedlyPlatform { }: any): Promise { return { platform: this.name, + color: [...this.color], + emoji: this.emoji, username: post_data.author, name: post_data.subreddit_name_prefixed, profile_url: `https://reddit.com/user/${post_data.author}`, @@ -147,9 +145,7 @@ export class Reddit extends EmbedlyPlatform { const embed = new Embed(data); - if (media.length > 10) { - media.length = 10; - } + // Media truncation will be handled by Embed.setMedia if (media.length > 0) { embed.setMedia(media); } diff --git a/packages/platforms/src/Threads.ts b/packages/platforms/src/Threads.ts index 7501178..07f6ab3 100644 --- a/packages/platforms/src/Threads.ts +++ b/packages/platforms/src/Threads.ts @@ -1,28 +1,26 @@ import { Embed } from "@embedly/builder"; -import { - EMBEDLY_FAILED_PLATFORM, - EMBEDLY_FETCH_PLATFORM -} from "@embedly/logging"; -import { THREADS_REGEX } from "@embedly/parser"; import { type BaseEmbedData, - EmbedlyPlatformType -} from "@embedly/types"; -import { EmbedlyPlatform } from "./Platform.ts"; + type CloudflareEnv, + EmbedlyPlatform +} from "./Platform.ts"; +import { EmbedlyPlatformType } from "./types.ts"; const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"; export class Threads extends EmbedlyPlatform { + readonly color = [0, 0, 0] as const; + readonly emoji = "<:threads:1413343483929956446>"; + readonly regex = + /threads\.net\/@.*\/post\/(?[A-Za-z0-9-_]+)/; + constructor() { - super(EmbedlyPlatformType.Threads, "threads", { - fetching: EMBEDLY_FETCH_PLATFORM(EmbedlyPlatformType.Threads), - failed: EMBEDLY_FAILED_PLATFORM(EmbedlyPlatformType.Threads) - }); + super(EmbedlyPlatformType.Threads, "threads"); } async parsePostId(url: string): Promise { - const match = THREADS_REGEX.exec(url)!; + const match = this.regex.exec(url)!; const { thread_shortcode } = match.groups!; const thread_id = thread_shortcode .split("") @@ -36,12 +34,9 @@ export class Threads extends EmbedlyPlatform { async fetchPost( thread_id: string, - env: { - EMBED_USER_AGENT: string; - THREADS_CSRF_TOKEN: string; - } + env?: Partial ): Promise { - const graphql = new URL(`https://www.threads.com/graphql/query`); + const graphql = new URL(`https://www.threads.net/graphql/query`); graphql.searchParams.set( "variables", JSON.stringify({ @@ -76,10 +71,10 @@ export class Threads extends EmbedlyPlatform { const resp = await fetch(graphql.toString(), { method: "POST", headers: { - "User-Agent": env.EMBED_USER_AGENT, + "User-Agent": env?.EMBED_USER_AGENT ?? "", "Content-Type": "application/x-www-form-urlencoded", "X-IG-App-ID": "238260118697367", - "X-CSRFTOKEN": env.THREADS_CSRF_TOKEN, + "X-CSRFTOKEN": env?.THREADS_CSRF_TOKEN ?? "", "X-FB-LSD": "UpH8MtbTBKi8Wbdbt_uZX3", "X-ASBD-ID": "359341", "Sec-Fetch-Site": "same-origin" @@ -128,12 +123,14 @@ export class Threads extends EmbedlyPlatform { transformRawData(raw_data: any): BaseEmbedData { return { platform: this.name, + color: [...this.color], + emoji: this.emoji, name: raw_data.user.full_name, username: raw_data.user.username, - profile_url: `https://threads.com/@${raw_data.user.username}`, + profile_url: `https://threads.net/@${raw_data.user.username}`, avatar_url: raw_data.user.profile_pic_url, timestamp: raw_data.taken_at, - url: `https://threads.com/@${raw_data.user.username}/post/${raw_data.code}`, + url: `https://threads.net/@${raw_data.user.username}/post/${raw_data.code}`, stats: { comments: raw_data.text_post_app_info.direct_reply_count, likes: raw_data.like_count, @@ -147,9 +144,7 @@ export class Threads extends EmbedlyPlatform { const embed = new Embed(this.transformRawData(post_data)); const media = this.parsePostMedia(post_data); if (media.length > 0) { - if (media.length > 10) { - media.length = 10; - } + // Media truncation will be handled by Embed.setMedia embed.setMedia(media); } diff --git a/packages/platforms/src/TikTok.ts b/packages/platforms/src/TikTok.ts index fa34b13..d3463ae 100644 --- a/packages/platforms/src/TikTok.ts +++ b/packages/platforms/src/TikTok.ts @@ -1,34 +1,39 @@ import { Embed } from "@embedly/builder"; -import { - EMBEDLY_FAILED_PLATFORM, - EMBEDLY_FETCH_PLATFORM -} from "@embedly/logging"; -import { TIKTOK_REGEX } from "@embedly/parser"; +import * as cheerio from "cheerio"; +import { CF_CACHE_OPTIONS } from "./constants.ts"; import { type BaseEmbedData, - EmbedlyPlatformType -} from "@embedly/types"; -import * as cheerio from "cheerio"; -import { EmbedlyPlatform } from "./Platform.ts"; + type CloudflareEnv, + EmbedlyPlatform +} from "./Platform.ts"; +import { EmbedlyPlatformType } from "./types.ts"; + +// Main regex for initial URL detection +const TIKTOK_REGEX_MAIN = /(https?:\/\/)?(?:[\w-]+\.)*tiktok\.com/; + +// Detailed regex for extracting user and video ID from expanded URL +const TIKTOK_REGEX_DETAIL = + /https:\/\/(?:m|www|vm)?\.?tiktok\.com\/(?@[\w.-]+)\/video\/(?\d+)/; export class TikTok extends EmbedlyPlatform { + readonly color = [57, 118, 132] as const; + readonly emoji = "<:tiktok:1386641825963708446>"; + readonly regex = TIKTOK_REGEX_MAIN; + constructor() { - super(EmbedlyPlatformType.TikTok, "tiktok", { - fetching: EMBEDLY_FETCH_PLATFORM(EmbedlyPlatformType.TikTok), - failed: EMBEDLY_FAILED_PLATFORM(EmbedlyPlatformType.TikTok) - }); + super(EmbedlyPlatformType.TikTok, "tiktok"); } async parsePostId(url: string): Promise { const req = await fetch(url, { redirect: "follow" }); - const match = TIKTOK_REGEX.exec(req.url)!; + const match = TIKTOK_REGEX_DETAIL.exec(req.url)!; const { tiktok_user, tiktok_id } = match.groups!; return `${tiktok_user}/${tiktok_id}`; } async fetchPost( post_id: string, - env: { EMBED_USER_AGENT: string } + env?: Partial ): Promise { const [tiktok_user, tiktok_id] = post_id.split("/"); const resp = await fetch( @@ -36,12 +41,9 @@ export class TikTok extends EmbedlyPlatform { { method: "GET", headers: { - "User-Agent": env.EMBED_USER_AGENT + "User-Agent": env?.EMBED_USER_AGENT ?? "" }, - cf: { - cacheTtl: 60 * 60 * 24, - cacheEverything: true - } + ...CF_CACHE_OPTIONS } ); if (!resp.ok) { @@ -58,6 +60,8 @@ export class TikTok extends EmbedlyPlatform { transformRawData(raw_data: any): BaseEmbedData { return { platform: this.name, + color: [...this.color], + emoji: this.emoji, name: raw_data.author.nickname, username: raw_data.author.uniqueId, profile_url: `https://tiktok.com/@${raw_data.author.uniqueId}`, diff --git a/packages/platforms/src/Twitter.ts b/packages/platforms/src/Twitter.ts index 7cfadb4..183db79 100644 --- a/packages/platforms/src/Twitter.ts +++ b/packages/platforms/src/Twitter.ts @@ -1,26 +1,21 @@ import { Embed } from "@embedly/builder"; -import { - EMBEDLY_FAILED_PLATFORM, - EMBEDLY_FETCH_PLATFORM -} from "@embedly/logging"; -import { TWITTER_REGEX } from "@embedly/parser"; -import { - type BaseEmbedData, - EmbedlyPlatformType -} from "@embedly/types"; import he from "he"; -import { EmbedlyPlatform } from "./Platform.ts"; +import { CF_CACHE_OPTIONS } from "./constants.ts"; +import { type BaseEmbedData, EmbedlyPlatform } from "./Platform.ts"; +import { EmbedlyPlatformType } from "./types.ts"; export class Twitter extends EmbedlyPlatform { + readonly color = [29, 161, 242] as const; + readonly emoji = "<:twitter:1386639732179599481>"; + readonly regex = + /(?:twitter|x).com\/.*\/status(?:es)?\/(?[^/?]+)/; + constructor() { - super(EmbedlyPlatformType.Twitter, "tweet", { - fetching: EMBEDLY_FETCH_PLATFORM(EmbedlyPlatformType.Twitter), - failed: EMBEDLY_FAILED_PLATFORM(EmbedlyPlatformType.Twitter) - }); + super(EmbedlyPlatformType.Twitter, "tweet"); } async parsePostId(url: string): Promise { - const match = TWITTER_REGEX.exec(url)!; + const match = this.regex.exec(url)!; const { tweet_id } = match.groups!; return tweet_id; } @@ -33,10 +28,7 @@ export class Twitter extends EmbedlyPlatform { headers: { "User-Agent": "Embedly/0.0.1" }, - cf: { - cacheTtl: 60 * 60 * 24, - cacheEverything: true - } + ...CF_CACHE_OPTIONS } ).then((r) => r.json() as Record); if (code !== 200) { @@ -69,6 +61,8 @@ export class Twitter extends EmbedlyPlatform { transformRawData(raw_data: any): BaseEmbedData { return { platform: this.name, + color: [...this.color], + emoji: this.emoji, name: raw_data.author.name, username: raw_data.author.screen_name, profile_url: `https://x.com/${raw_data.author.screen_name}`, diff --git a/packages/platforms/src/constants.ts b/packages/platforms/src/constants.ts new file mode 100644 index 0000000..a68236c --- /dev/null +++ b/packages/platforms/src/constants.ts @@ -0,0 +1,11 @@ +// Cloudflare cache options for platform API calls +export const CF_CACHE_OPTIONS = { + cf: { + cacheTtl: 60 * 60 * 24, + cacheEverything: true + } +} as const; + +// Generic link regex for detecting URLs +export const GENERIC_LINK_REGEX = + /\b((?:[a-z][\w-]+:(?:\/{1,3}|[a-z0-9%])|www\d{0,3}[.]|[a-z0-9.-]+[.][a-z]{2,4}\/)(?:[^\s()<>]+|\(([^\s()<>]+|(\([^\s()<>]+\)))*\))+(?:\(([^\s()<>]+|(\([^\s()<>]+\)))*\)|[^\s`!()[\]{};:'".,<>?«»""'']))/i; diff --git a/packages/platforms/src/main.ts b/packages/platforms/src/main.ts index a3560a2..83c4389 100644 --- a/packages/platforms/src/main.ts +++ b/packages/platforms/src/main.ts @@ -1,16 +1,58 @@ -import { EmbedlyPlatformType } from "@embedly/types"; import { CBC } from "./CBC.ts"; import { Instagram } from "./Instagram.ts"; +import { registerPlatformDetector } from "./parsing.ts"; import { Reddit } from "./Reddit.ts"; import { Threads } from "./Threads.ts"; import { TikTok } from "./TikTok.ts"; import { Twitter } from "./Twitter.ts"; +import { EmbedlyPlatformType } from "./types.ts"; -export default { +export const Platforms = { [EmbedlyPlatformType.Twitter]: new Twitter(), [EmbedlyPlatformType.Instagram]: new Instagram(), [EmbedlyPlatformType.TikTok]: new TikTok(), [EmbedlyPlatformType.CBC]: new CBC(), [EmbedlyPlatformType.Threads]: new Threads(), [EmbedlyPlatformType.Reddit]: new Reddit() -}; +} as const; + +export const EmbedlyPlatformColors = Object.fromEntries( + Object.entries(Platforms).map(([key, platform]) => [ + key, + platform.color + ]) +) as Record; + +export const platformEmojis = Object.fromEntries( + Object.entries(Platforms).map(([key, platform]) => [ + key, + platform.emoji + ]) +) as Record; + +registerPlatformDetector((url: string) => { + for (const [name, platform] of Object.entries(Platforms)) { + if (platform.matchesUrl(url)) { + return { type: name as EmbedlyPlatformType }; + } + } + return null; +}); + +export { CF_CACHE_OPTIONS, GENERIC_LINK_REGEX } from "./constants.ts"; +export { type CloudflareEnv, EmbedlyPlatform } from "./Platform.ts"; +export { + getPlatformFromURL, + hasLink, + isEscaped, + isSpoiler +} from "./parsing.ts"; +export { + EmbedlyPlatformType, + type Emojis, + emojis, + type StatEmojis, + statEmojis +} from "./types.ts"; + +export default Platforms; diff --git a/packages/platforms/src/parsing.ts b/packages/platforms/src/parsing.ts new file mode 100644 index 0000000..2c77830 --- /dev/null +++ b/packages/platforms/src/parsing.ts @@ -0,0 +1,42 @@ +import { GENERIC_LINK_REGEX } from "./constants.ts"; +import type { EmbedlyPlatformType } from "./types.ts"; + +/** + * Check if content contains any link + */ +export function hasLink(content: string): boolean { + return GENERIC_LINK_REGEX.test(content); +} + +/** + * Check if a URL is wrapped in spoiler tags (||url||) + */ +export function isSpoiler(url: string, content: string): boolean { + return content + .split("||") + .some((part, ind) => ind % 2 === 1 && part.includes(url)); +} + +/** + * Check if a URL is escaped with angle brackets () + */ +export function isEscaped(url: string, content: string): boolean { + return content.includes(`<${url}>`); +} + +/** + * Data-driven platform detection - will be populated by main.ts + * after platform instances are created + */ +export let getPlatformFromURL: ( + url: string +) => null | { type: EmbedlyPlatformType }; + +/** + * Register the platform detection function (called from main.ts) + */ +export function registerPlatformDetector( + detector: (url: string) => null | { type: EmbedlyPlatformType } +): void { + getPlatformFromURL = detector; +} diff --git a/packages/platforms/src/types.ts b/packages/platforms/src/types.ts new file mode 100644 index 0000000..25762dd --- /dev/null +++ b/packages/platforms/src/types.ts @@ -0,0 +1,43 @@ +export const EmbedlyPlatformType = { + Twitter: "Twitter", + Instagram: "Instagram", + TikTok: "TikTok", + CBC: "cbc.ca", + Threads: "Threads", + Reddit: "Reddit" +} as const; + +export type EmbedlyPlatformType = + (typeof EmbedlyPlatformType)[keyof typeof EmbedlyPlatformType]; + +export interface StatEmojis { + comments: string; + reposts: string; + likes: string; + bookmarks: string; + views: string; + reply: string; + quote: string; +} + +export const statEmojis: StatEmojis = { + comments: "<:comment:1386639521373753374>", + reposts: "<:repost:1386639564143198349>", + likes: "<:like:1386639662772391987>", + bookmarks: "<:bookmark:1386639640433529014>", + views: "<:view:1386639685237084292>", + reply: "<:reply:1386639619768058007>", + quote: "<:quote:1389657738480713838>" +}; + +export type Emojis = StatEmojis & Record; + +export const emojis: Emojis = { + ...statEmojis, + [EmbedlyPlatformType.Twitter]: "<:twitter:1386639732179599481>", + [EmbedlyPlatformType.Instagram]: "<:instagram:1386639712013254748>", + [EmbedlyPlatformType.TikTok]: "<:tiktok:1386641825963708446>", + [EmbedlyPlatformType.CBC]: "<:cbc:1409997044495683674>", + [EmbedlyPlatformType.Threads]: "<:threads:1413343483929956446>", + [EmbedlyPlatformType.Reddit]: "<:reddit:1461320093240655922>" +}; diff --git a/packages/types/CHANGELOG.md b/packages/types/CHANGELOG.md deleted file mode 100644 index f39eb73..0000000 --- a/packages/types/CHANGELOG.md +++ /dev/null @@ -1,117 +0,0 @@ -# @embedly/types - -## 0.11.0 - -## 0.10.0 - -### Patch Changes - -- [#36](https://github.com/embed-team/embedly/pull/36) [`dde5d23`](https://github.com/embed-team/embedly/commit/dde5d23d2d3705dc743fb772d602f64d85069f8c) Thanks [@ItsRauf](https://github.com/ItsRauf)! - feat(ci): split github actions across multiple files - -## 0.9.1 - -### Patch Changes - -- [#34](https://github.com/embed-team/embedly/pull/34) [`d8495b4`](https://github.com/embed-team/embedly/commit/d8495b40953bce13cb528dfb28f467cdbf19d07c) Thanks [@ItsRauf](https://github.com/ItsRauf)! - feat(ci): split github actions across multiple files - -## 0.9.0 - -## 0.8.0 - -## 0.7.1 - -## 0.7.0 - -## 0.6.0 - -### Minor Changes - -- [#22](https://github.com/embed-team/embedly/pull/22) [`327acfe`](https://github.com/embed-team/embedly/commit/327acfe0c405fd57b65643e881bd4978af47e0bb) Thanks [@ItsRauf](https://github.com/ItsRauf)! - refactor embed builder and fix delete command - -### Patch Changes - -- [#22](https://github.com/embed-team/embedly/pull/22) [`327acfe`](https://github.com/embed-team/embedly/commit/327acfe0c405fd57b65643e881bd4978af47e0bb) Thanks [@ItsRauf](https://github.com/ItsRauf)! - replies now show in the correct order - -## 0.5.3 - -### Patch Changes - -- [#20](https://github.com/embed-team/embedly/pull/20) [`455d9b1`](https://github.com/embed-team/embedly/commit/455d9b19b0c8662a85799557fedd8b9bfae2cc33) Thanks [@ItsRauf](https://github.com/ItsRauf)! - sane spoiler and escape detection - -## 0.5.2 - -## 0.5.1 - -## 0.5.0 - -## 0.4.1 - -### Patch Changes - -- [#8](https://github.com/embed-team/embedly/pull/8) [`4386504`](https://github.com/embed-team/embedly/commit/438650487368ec722bc75801ccf5f495be62a485) Thanks [@ItsRauf](https://github.com/ItsRauf)! - fix broken spoiler regex - -## 0.4.0 - -### Minor Changes - -- [#6](https://github.com/embed-team/embedly/pull/6) [`14785b7`](https://github.com/embed-team/embedly/commit/14785b70759445b7d402a3d63bca72993239f5b3) Thanks [@ItsRauf](https://github.com/ItsRauf)! - add a spoiler flag and support masked links in messages - -## 0.3.3 - -### Patch Changes - -- [`6d3fccd`](https://github.com/embed-team/embedly/commit/6d3fccd8190a8b697e0ee93edc8c81affb036f01) Thanks [@ItsRauf](https://github.com/ItsRauf)! - added DX and CI tooling - -## 0.3.2 - -### Patch Changes - -- fixed title for cbc embeds where username doesn't exist - -## 0.3.1 - -### Patch Changes - -- escape markdown in embed description - -## 0.3.0 - -### Minor Changes - -- added threads and fixed a few emoji issues - -## 0.2.0 - -### Minor Changes - -- added cbc.ca, fixed tiktok, added media proxy - -## 0.1.4 - -### Patch Changes - -- added a fallback error for random failures and added multi-embedding for messages - -## 0.1.3 - -### Patch Changes - -- fixed tiktok embeds and added new instagram share link handling - -## 0.1.2 - -### Patch Changes - -- rewrote api and bot embed handling to make the api only handle scraping - -## 0.1.1 - -### Patch Changes - -- added sidebar color to containers - -## 0.1.0 - -### Minor Changes - -- this is the first oss version of embedly :3 diff --git a/packages/types/biome.json b/packages/types/biome.json deleted file mode 100644 index 167b18f..0000000 --- a/packages/types/biome.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "root": false, - "$schema": "./node_modules/@biomejs/biome/configuration_schema.json", - "extends": ["@embedly/config/biome.json"] -} diff --git a/packages/types/package.json b/packages/types/package.json deleted file mode 100644 index 416da01..0000000 --- a/packages/types/package.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "name": "@embedly/types", - "version": "0.11.0", - "private": true, - "type": "module", - "main": "./dist/main.js", - "module": "./dist/main.js", - "types": "./dist/main.d.ts", - "exports": { - "import": { - "types": "./dist/main.d.ts", - "default": "./dist/main.js" - } - }, - "scripts": { - "lint": "biome ci ./src/*", - "build": "pkgroll --sourcemap=inline && tsc" - }, - "devDependencies": { - "@biomejs/biome": "2.3.10", - "@embedly/config": "workspace:*", - "pkgroll": "^2.21.4", - "typescript": "^5.9.3" - }, - "dependencies": { - "discord-api-types": "^0.38.37" - } -} diff --git a/packages/types/src/main.ts b/packages/types/src/main.ts deleted file mode 100644 index b01958b..0000000 --- a/packages/types/src/main.ts +++ /dev/null @@ -1,70 +0,0 @@ -import type { APIMediaGalleryItem } from "discord-api-types/v10"; - -export enum EmbedlyPlatformType { - Twitter = "Twitter", - Instagram = "Instagram", - TikTok = "TikTok", - CBC = "cbc.ca", - Threads = "Threads", - Reddit = "Reddit" -} - -export const EmbedlyPlatformColors: Record< - EmbedlyPlatformType, - [red: number, blue: number, green: number] -> = { - [EmbedlyPlatformType.Twitter]: [29, 161, 242], - [EmbedlyPlatformType.Instagram]: [225, 48, 108], - [EmbedlyPlatformType.TikTok]: [57, 118, 132], - [EmbedlyPlatformType.CBC]: [215, 36, 42], - [EmbedlyPlatformType.Threads]: [0, 0, 0], - [EmbedlyPlatformType.Reddit]: [255, 86, 0] -}; - -export interface StatsData { - comments: number; - reposts?: number; - likes: number; - bookmarks?: number; - views?: number; -} - -export interface BaseEmbedData { - platform: EmbedlyPlatformType; - name: string; - username?: string; - profile_url?: string; - avatar_url: string; - url: string; - timestamp: number; - stats?: StatsData; - description?: string; - media?: APIMediaGalleryItem[]; -} -export type BaseEmbedDataWithoutPlatform = Omit< - BaseEmbedData, - "platform" ->; - -export type Emojis = { - [K in keyof Required]: string; -} & { [K in EmbedlyPlatformType]: string } & { - reply: string; - quote: string; -}; - -export const emojis: Emojis = { - comments: "<:comment:1386639521373753374>", - reposts: "<:repost:1386639564143198349>", - likes: "<:like:1386639662772391987>", - bookmarks: "<:bookmark:1386639640433529014>", - views: "<:view:1386639685237084292>", - reply: "<:reply:1386639619768058007>", - quote: "<:quote:1389657738480713838>", - [EmbedlyPlatformType.Twitter]: "<:twitter:1386639732179599481>", - [EmbedlyPlatformType.Instagram]: "<:instagram:1386639712013254748>", - [EmbedlyPlatformType.TikTok]: "<:tiktok:1386641825963708446>", - [EmbedlyPlatformType.CBC]: "<:cbc:1409997044495683674>", - [EmbedlyPlatformType.Threads]: "<:threads:1413343483929956446>", - [EmbedlyPlatformType.Reddit]: "<:reddit:1461320093240655922>" -}; diff --git a/packages/types/tsconfig.json b/packages/types/tsconfig.json deleted file mode 100644 index 3a138a9..0000000 --- a/packages/types/tsconfig.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "extends": "@embedly/config/tsconfig.json", - "compilerOptions": { - "lib": ["WebWorker", "ESNext"], - "outDir": "./dist" - }, - "exclude": ["node_modules"], - "include": ["src"] -} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f795203..e34161a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -56,15 +56,9 @@ importers: '@embedly/logging': specifier: workspace:* version: link:../../packages/logging - '@embedly/parser': - specifier: workspace:* - version: link:../../packages/parser '@embedly/platforms': specifier: workspace:* version: link:../../packages/platforms - '@embedly/types': - specifier: workspace:* - version: link:../../packages/types '@logtail/edge': specifier: ^0.5.7 version: 0.5.7(@cloudflare/workers-types@4.20251220.0) @@ -148,9 +142,6 @@ importers: '@embedly/logging': specifier: workspace:* version: link:../../packages/logging - '@embedly/parser': - specifier: workspace:* - version: link:../../packages/parser '@embedly/platforms': specifier: workspace:* version: link:../../packages/platforms @@ -175,12 +166,6 @@ importers: '@discordjs/builders': specifier: ^1.13.1 version: 1.13.1 - '@embedly/parser': - specifier: workspace:* - version: link:../parser - '@embedly/types': - specifier: workspace:* - version: link:../types discord-api-types: specifier: ^0.38.37 version: 0.38.37 @@ -205,28 +190,6 @@ importers: version: 2.3.10 packages/logging: - devDependencies: - '@biomejs/biome': - specifier: 2.3.10 - version: 2.3.10 - '@embedly/config': - specifier: workspace:* - version: link:../config - '@embedly/types': - specifier: workspace:* - version: link:../types - pkgroll: - specifier: ^2.21.4 - version: 2.21.4(typescript@5.9.3) - typescript: - specifier: ^5.9.3 - version: 5.9.3 - - packages/parser: - dependencies: - '@embedly/types': - specifier: workspace:* - version: link:../types devDependencies: '@biomejs/biome': specifier: 2.3.10 @@ -249,12 +212,6 @@ importers: '@embedly/logging': specifier: workspace:* version: link:../logging - '@embedly/parser': - specifier: workspace:* - version: link:../parser - '@embedly/types': - specifier: workspace:* - version: link:../types '@types/he': specifier: ^1.2.3 version: 1.2.3 @@ -284,25 +241,6 @@ importers: specifier: ^5.9.3 version: 5.9.3 - packages/types: - dependencies: - discord-api-types: - specifier: ^0.38.37 - version: 0.38.37 - devDependencies: - '@biomejs/biome': - specifier: 2.3.10 - version: 2.3.10 - '@embedly/config': - specifier: workspace:* - version: link:../config - pkgroll: - specifier: ^2.21.4 - version: 2.21.4(typescript@5.9.3) - typescript: - specifier: ^5.9.3 - version: 5.9.3 - packages: '@babel/code-frame@7.27.1': From 6de074cf385a1a14c65a8ac18f5cc22cfa7c5eac Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 22 Jan 2026 17:02:13 -0600 Subject: [PATCH 02/20] build(deps): bump wrangler from 4.56.0 to 4.59.1 (#41) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- apps/api/package.json | 2 +- pnpm-lock.yaml | 437 +++++++++++++++++++++++------------------- 2 files changed, 237 insertions(+), 202 deletions(-) diff --git a/apps/api/package.json b/apps/api/package.json index 3b2a3eb..7af4721 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -29,7 +29,7 @@ "@logtail/node": "^0.5.6", "discord-verify": "^1.2.0", "elysia": "^1.4.19", - "wrangler": "^4.56.0" + "wrangler": "^4.59.1" }, "devDependencies": { "@biomejs/biome": "2.3.10", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e34161a..919561e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -16,19 +16,19 @@ importers: version: 0.5.2 '@changesets/cli': specifier: 2.29.8 - version: 2.29.8(@types/node@25.0.3) + version: 2.29.8(@types/node@25.0.9) '@changesets/config': specifier: ^3.1.2 version: 3.1.2 '@commitlint/cli': specifier: ^20.2.0 - version: 20.2.0(@types/node@25.0.3)(typescript@5.9.3) + version: 20.2.0(@types/node@25.0.9)(typescript@5.9.3) '@embedly/config': specifier: workspace:* version: link:packages/config commitlint: specifier: ^20.2.0 - version: 20.2.0(@types/node@25.0.3)(typescript@5.9.3) + version: 20.2.0(@types/node@25.0.9)(typescript@5.9.3) commitlint-config-gitmoji: specifier: ^2.3.1 version: 2.3.1 @@ -72,8 +72,8 @@ importers: specifier: ^1.4.19 version: 1.4.19(@sinclair/typebox@0.34.38)(@types/bun@1.3.5)(exact-mirror@0.1.2(@sinclair/typebox@0.34.38))(file-type@21.0.0)(openapi-types@12.1.3)(typescript@5.9.3) wrangler: - specifier: ^4.56.0 - version: 4.56.0(@cloudflare/workers-types@4.20251220.0) + specifier: ^4.59.1 + version: 4.59.1(@cloudflare/workers-types@4.20251220.0) devDependencies: '@biomejs/biome': specifier: 2.3.10 @@ -308,8 +308,8 @@ packages: cpu: [x64] os: [win32] - '@borewit/text-codec@0.1.1': - resolution: {integrity: sha512-5L/uBxmjaCIX5h8Z+uu+kA9BQLkc/Wl06UGR5ajNRxu+/XjonB5i8JpgFMrPj3LXTCPA0pv8yxUvbUi+QthGGA==} + '@borewit/text-codec@0.2.1': + resolution: {integrity: sha512-k7vvKPbf7J2fZ5klGRD9AeKfUvojuZIQ3BT5u7Jfv+puwXkUBUT5PVyMDfJZpy30CBDXGMgw7fguK/lpOMBvgw==} '@changesets/apply-release-plan@7.0.14': resolution: {integrity: sha512-ddBvf9PHdy2YY0OUiEl3TV78mH9sckndJR14QAt87KLEbIov81XO0q0QAmvooBxXlqRRP8I9B7XOzZwQG7JkWA==} @@ -376,8 +376,8 @@ packages: resolution: {integrity: sha512-Nu8ahitGFFJztxUml9oD/DLb7Z28C8cd8F46IVQ7y5Btz575pvMY8AqZsXkX7Gds29eCKdMgIHjIvzskHgPSFg==} engines: {node: '>=18.0.0'} - '@cloudflare/unenv-preset@2.7.13': - resolution: {integrity: sha512-NulO1H8R/DzsJguLC0ndMuk4Ufv0KSlN+E54ay9rn9ZCQo0kpAPwwh3LhgpZ96a3Dr6L9LqW57M4CqC34iLOvw==} + '@cloudflare/unenv-preset@2.9.0': + resolution: {integrity: sha512-99nEvuOTCGGGRNaIat8UVVXJ27aZK+U09SYDp0kVjQLwC9wyxcrQ28IqLwrQq2DjWLmBI1+UalGJzdPqYgPlRw==} peerDependencies: unenv: 2.0.0-rc.24 workerd: ^1.20251202.0 @@ -385,32 +385,32 @@ packages: workerd: optional: true - '@cloudflare/workerd-darwin-64@1.20251217.0': - resolution: {integrity: sha512-DN6vT+9ho61d/1/YuILW4VS+N1JBLaixWRL1vqNmhgbf8J8VHwWWotrRruEUYigJKx2yZyw6YsasE+yLXgx/Fw==} + '@cloudflare/workerd-darwin-64@1.20260111.0': + resolution: {integrity: sha512-UGAjrGLev2/CMLZy7b+v1NIXA4Hupc/QJBFlJwMqldywMcJ/iEqvuUYYuVI2wZXuXeWkgmgFP87oFDQsg78YTQ==} engines: {node: '>=16'} cpu: [x64] os: [darwin] - '@cloudflare/workerd-darwin-arm64@1.20251217.0': - resolution: {integrity: sha512-5nZOpRTkHmtcTc4Wbr1mj/O3dLb6aHZSiJuVBgtdbVcVmOXueSay3hnw1PXEyR+vpTKGUPkM+omUIslKHWnXDw==} + '@cloudflare/workerd-darwin-arm64@1.20260111.0': + resolution: {integrity: sha512-YFAZwidLCQVa6rKCCaiWrhA+eh87a7MUhyd9lat3KSbLBAGpYM+ORpyTXpi2Gjm3j6Mp1e/wtzcFTSeMIy2UqA==} engines: {node: '>=16'} cpu: [arm64] os: [darwin] - '@cloudflare/workerd-linux-64@1.20251217.0': - resolution: {integrity: sha512-uoPGhMaZVXPpCsU0oG3HQzyVpXCGi5rU+jcHRjUI7DXM4EwctBGvZ380Knkja36qtl+ZvSKVR1pUFSGdK+45Pg==} + '@cloudflare/workerd-linux-64@1.20260111.0': + resolution: {integrity: sha512-zx1GW6FwfOBjCV7QUCRzGRkViUtn3Is/zaaVPmm57xyy9sjtInx6/SdeBr2Y45tx9AnOP1CnaOFFdmH1P7VIEg==} engines: {node: '>=16'} cpu: [x64] os: [linux] - '@cloudflare/workerd-linux-arm64@1.20251217.0': - resolution: {integrity: sha512-ixHnHKsiz1Xko+eDgCJOZ7EEUZKtmnYq3AjW3nkVcLFypSLks4C29E45zVewdaN4wq8sCLeyQCl6r1kS17+DQQ==} + '@cloudflare/workerd-linux-arm64@1.20260111.0': + resolution: {integrity: sha512-wFVKxNvCyjRaAcgiSnJNJAmIos3p3Vv6Uhf4pFUZ9JIxr69GNlLWlm9SdCPvtwNFAjzSoDaKzDwjj5xqpuCS6Q==} engines: {node: '>=16'} cpu: [arm64] os: [linux] - '@cloudflare/workerd-windows-64@1.20251217.0': - resolution: {integrity: sha512-rP6USX+7ctynz3AtmKi+EvlLP3Xdr1ETrSdcnv693/I5QdUwBxq4yE1Lj6CV7GJizX6opXKYg8QMq0Q4eB9zRQ==} + '@cloudflare/workerd-windows-64@1.20260111.0': + resolution: {integrity: sha512-zWgd77L7OI1BxgBbG+2gybDahIMgPX5iNo6e3LqcEz1Xm3KfiqgnDyMBcxeQ7xDrj7fHUGAlc//QnKvDchuUoQ==} engines: {node: '>=16'} cpu: [x64] os: [win32] @@ -532,8 +532,8 @@ packages: peerDependencies: elysia: '>= 1.4.0' - '@emnapi/runtime@1.7.1': - resolution: {integrity: sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA==} + '@emnapi/runtime@1.8.1': + resolution: {integrity: sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==} '@esbuild/aix-ppc64@0.26.0': resolution: {integrity: sha512-hj0sKNCQOOo2fgyII3clmJXP28VhgDfU5iy3GNHlWO76KG6N7x4D9ezH5lJtQTG+1J6MFDAJXC1qsI+W+LvZoA==} @@ -1012,107 +1012,139 @@ packages: '@gitmoji/parser-opts@1.4.0': resolution: {integrity: sha512-zzmx/vtpdB/ijjUm7u9OzHNCXWKpSbzVEgVzOzhilMgoTBlUDyInZFUtiCTV+Wf4oCP9nxGa/kQGQFfN+XLH1g==} - '@img/sharp-darwin-arm64@0.33.5': - resolution: {integrity: sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==} + '@img/colour@1.0.0': + resolution: {integrity: sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==} + engines: {node: '>=18'} + + '@img/sharp-darwin-arm64@0.34.5': + resolution: {integrity: sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [darwin] - '@img/sharp-darwin-x64@0.33.5': - resolution: {integrity: sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==} + '@img/sharp-darwin-x64@0.34.5': + resolution: {integrity: sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [darwin] - '@img/sharp-libvips-darwin-arm64@1.0.4': - resolution: {integrity: sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==} + '@img/sharp-libvips-darwin-arm64@1.2.4': + resolution: {integrity: sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==} cpu: [arm64] os: [darwin] - '@img/sharp-libvips-darwin-x64@1.0.4': - resolution: {integrity: sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==} + '@img/sharp-libvips-darwin-x64@1.2.4': + resolution: {integrity: sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==} cpu: [x64] os: [darwin] - '@img/sharp-libvips-linux-arm64@1.0.4': - resolution: {integrity: sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==} + '@img/sharp-libvips-linux-arm64@1.2.4': + resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==} cpu: [arm64] os: [linux] - '@img/sharp-libvips-linux-arm@1.0.5': - resolution: {integrity: sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==} + '@img/sharp-libvips-linux-arm@1.2.4': + resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==} cpu: [arm] os: [linux] - '@img/sharp-libvips-linux-s390x@1.0.4': - resolution: {integrity: sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==} + '@img/sharp-libvips-linux-ppc64@1.2.4': + resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==} + cpu: [ppc64] + os: [linux] + + '@img/sharp-libvips-linux-riscv64@1.2.4': + resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==} + cpu: [riscv64] + os: [linux] + + '@img/sharp-libvips-linux-s390x@1.2.4': + resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==} cpu: [s390x] os: [linux] - '@img/sharp-libvips-linux-x64@1.0.4': - resolution: {integrity: sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==} + '@img/sharp-libvips-linux-x64@1.2.4': + resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==} cpu: [x64] os: [linux] - '@img/sharp-libvips-linuxmusl-arm64@1.0.4': - resolution: {integrity: sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==} + '@img/sharp-libvips-linuxmusl-arm64@1.2.4': + resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==} cpu: [arm64] os: [linux] - '@img/sharp-libvips-linuxmusl-x64@1.0.4': - resolution: {integrity: sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==} + '@img/sharp-libvips-linuxmusl-x64@1.2.4': + resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==} cpu: [x64] os: [linux] - '@img/sharp-linux-arm64@0.33.5': - resolution: {integrity: sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==} + '@img/sharp-linux-arm64@0.34.5': + resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] - '@img/sharp-linux-arm@0.33.5': - resolution: {integrity: sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==} + '@img/sharp-linux-arm@0.34.5': + resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm] os: [linux] - '@img/sharp-linux-s390x@0.33.5': - resolution: {integrity: sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==} + '@img/sharp-linux-ppc64@0.34.5': + resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [ppc64] + os: [linux] + + '@img/sharp-linux-riscv64@0.34.5': + resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [riscv64] + os: [linux] + + '@img/sharp-linux-s390x@0.34.5': + resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [s390x] os: [linux] - '@img/sharp-linux-x64@0.33.5': - resolution: {integrity: sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==} + '@img/sharp-linux-x64@0.34.5': + resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] - '@img/sharp-linuxmusl-arm64@0.33.5': - resolution: {integrity: sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==} + '@img/sharp-linuxmusl-arm64@0.34.5': + resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] - '@img/sharp-linuxmusl-x64@0.33.5': - resolution: {integrity: sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==} + '@img/sharp-linuxmusl-x64@0.34.5': + resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] - '@img/sharp-wasm32@0.33.5': - resolution: {integrity: sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==} + '@img/sharp-wasm32@0.34.5': + resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [wasm32] - '@img/sharp-win32-ia32@0.33.5': - resolution: {integrity: sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==} + '@img/sharp-win32-arm64@0.34.5': + resolution: {integrity: sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [win32] + + '@img/sharp-win32-ia32@0.34.5': + resolution: {integrity: sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [ia32] os: [win32] - '@img/sharp-win32-x64@0.33.5': - resolution: {integrity: sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==} + '@img/sharp-win32-x64@0.34.5': + resolution: {integrity: sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [win32] @@ -1439,12 +1471,12 @@ packages: '@sinclair/typebox@0.34.38': resolution: {integrity: sha512-HpkxMmc2XmZKhvaKIZZThlHmx1L0I/V1hWK1NubtlFnr6ZqdiOpV72TKudZUNQjZNsyDBay72qFEhEvb+bcwcA==} - '@sindresorhus/is@7.1.1': - resolution: {integrity: sha512-rO92VvpgMc3kfiTjGT52LEtJ8Yc5kCWhZjLQ3LwlA4pSgPpQO7bVpYXParOD8Jwf+cVQECJo3yP/4I8aZtUQTQ==} + '@sindresorhus/is@7.2.0': + resolution: {integrity: sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw==} engines: {node: '>=18'} - '@speed-highlight/core@1.2.12': - resolution: {integrity: sha512-uilwrK0Ygyri5dToHYdZSjcvpS2ZwX0w5aSt3GCEN9hrjxWCoeV4Z2DTXuxjwbntaLQIEEAlCeNQss5SoHvAEA==} + '@speed-highlight/core@1.2.14': + resolution: {integrity: sha512-G4ewlBNhUtlLvrJTb88d2mdy2KRijzs4UhnlrOSRT4bmjh/IqNElZa3zkrZ+TC47TwtlDWzVLFADljF1Ijp5hA==} '@tokenizer/inflate@0.2.7': resolution: {integrity: sha512-MADQgmZT1eKjp06jpI2yozxaU9uVs4GzzgSL+uEq7bVcJ9V1ZXQkeGNql1fsSI0gMy1vhvNTNbUqrx+pZfJVmg==} @@ -1492,6 +1524,9 @@ packages: '@types/node@25.0.3': resolution: {integrity: sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA==} + '@types/node@25.0.9': + resolution: {integrity: sha512-/rpCXHlCWeqClNBwUhDcusJxXYDjZTyE8v5oTO7WbL8eij2nKhUeU89/6xgjU7N4/Vh3He0BtyhJdQbDyhiXAw==} + '@types/qs@6.14.0': resolution: {integrity: sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==} @@ -1711,13 +1746,6 @@ packages: color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} - color-string@1.9.1: - resolution: {integrity: sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==} - - color@4.2.3: - resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==} - engines: {node: '>=12.5.0'} - commitlint-config-gitmoji@2.3.1: resolution: {integrity: sha512-T15ssbsyNc6szHlnGWo0/xvIA1mObqM70E9TwKNVTpksxhm+OdFht8hvDdKJAVi4nlZX5tcfTeILOi7SHBGH3w==} @@ -2224,9 +2252,6 @@ packages: is-arrayish@0.2.1: resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} - is-arrayish@0.3.4: - resolution: {integrity: sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==} - is-core-module@2.16.1: resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==} engines: {node: '>= 0.4'} @@ -2508,8 +2533,8 @@ packages: resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==} engines: {node: '>=18'} - miniflare@4.20251217.0: - resolution: {integrity: sha512-8xsTQbPS6YV+ABZl9qiJIbsum6hbpbhqiyKpOVdzZrhK+1N8EFpT8R6aBZff7kezGmxYZSntjgjqTwJmj3JLgA==} + miniflare@4.20260111.0: + resolution: {integrity: sha512-pUsbDlumPaTzliA+J9HMAM74nLR8wqpCQNOESximab51jAfvL7ZaP5Npzh4PWNV0Jfq28tlqazakuJcw6w5qlA==} engines: {node: '>=18.0.0'} hasBin: true @@ -2831,8 +2856,8 @@ packages: resolution: {integrity: sha512-3NnuWfM6vBYoy5gZFvHiYsVbafvI9vZv/+jlIigFn4oP4zjNPK3LhcY0xSCgeb1a5L8jO71Mit9LlNoi2UfDDQ==} engines: {node: '>=10'} - sharp@0.33.5: - resolution: {integrity: sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==} + sharp@0.34.5: + resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} shebang-command@2.0.0: @@ -2850,9 +2875,6 @@ packages: resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} engines: {node: '>=14'} - simple-swizzle@0.2.4: - resolution: {integrity: sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==} - slash@3.0.0: resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} engines: {node: '>=8'} @@ -2967,8 +2989,8 @@ packages: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} - token-types@6.1.1: - resolution: {integrity: sha512-kh9LVIWH5CnL63Ipf0jhlBIy0UsrMj/NJDfpsy1SqOXlLKEVyXXYrnFxFT1yOOYVGBSApeVnjPw/sBz5BfEjAQ==} + token-types@6.1.2: + resolution: {integrity: sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww==} engines: {node: '>=14.16'} tr46@0.0.3: @@ -3107,18 +3129,17 @@ packages: resolution: {integrity: sha512-c9bZp7b5YtRj2wOe6dlj32MK+Bx/M/d+9VB2SHM1OtsUHR0aV0tdP6DWh/iMt0kWi1t5g1Iudu6hQRNd1A4PVA==} engines: {node: '>=18'} - workerd@1.20251217.0: - resolution: {integrity: sha512-s3mHDSWwHTduyY8kpHOsl27ZJ4ziDBJlc18PfBvNMqNnhO7yBeemlxH7bo7yQyU1foJrIZ6IENHDDg0Z9N8zQA==} + workerd@1.20260111.0: + resolution: {integrity: sha512-ov6Pt4k6d/ALfJja/EIHohT9IrY/f6GAa0arWEPat2qekp78xHbVM7jSxNWAMbaE7ZmnQQIFEGD1ZhAWZmQKIg==} engines: {node: '>=16'} hasBin: true - wrangler@4.56.0: - resolution: {integrity: sha512-Nqi8duQeRbA+31QrD6QlWHW3IZVnuuRxMy7DEg46deUzywivmaRV/euBN5KKXDPtA24VyhYsK7I0tkb7P5DM2w==} + wrangler@4.59.1: + resolution: {integrity: sha512-5DddGSNxHd6dOjREWTDQdovQlZ1Lh80NNRXZFQ4/CrK3fNyVIBj9tqCs9pmXMNrKQ/AnKNeYzEs/l1kr8rHhOg==} engines: {node: '>=20.0.0'} - deprecated: Version 4.55.0 and 4.56.0 can incorrectly automatically delegate 'wrangler deploy' to 'opennextjs-cloudflare'. Use an older or newer version. hasBin: true peerDependencies: - '@cloudflare/workers-types': ^4.20251217.0 + '@cloudflare/workers-types': ^4.20260111.0 peerDependenciesMeta: '@cloudflare/workers-types': optional: true @@ -3194,8 +3215,8 @@ packages: youch@4.1.0-beta.10: resolution: {integrity: sha512-rLfVLB4FgQneDr0dv1oddCVZmKjcJ6yX6mS4pU82Mq/Dt9a3cLZQ62pDBL4AUO+uVrCvtWz3ZFUL2HFAFJ/BXQ==} - zod@3.22.3: - resolution: {integrity: sha512-EjIevzuJRiRPbVH4mGc8nApb/lVLKVpmUhAaR5R5doKGfAnGJ6Gr3CViAVjP+4FWSxCsybeWQdcgCtbX+7oZug==} + zod@3.25.76: + resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} snapshots: @@ -3244,7 +3265,7 @@ snapshots: '@biomejs/cli-win32-x64@2.3.10': optional: true - '@borewit/text-codec@0.1.1': {} + '@borewit/text-codec@0.2.1': {} '@changesets/apply-release-plan@7.0.14': dependencies: @@ -3283,7 +3304,7 @@ snapshots: transitivePeerDependencies: - encoding - '@changesets/cli@2.29.8(@types/node@25.0.3)': + '@changesets/cli@2.29.8(@types/node@25.0.9)': dependencies: '@changesets/apply-release-plan': 7.0.14 '@changesets/assemble-release-plan': 6.0.9 @@ -3299,7 +3320,7 @@ snapshots: '@changesets/should-skip-package': 0.1.2 '@changesets/types': 6.1.0 '@changesets/write': 0.4.0 - '@inquirer/external-editor': 1.0.3(@types/node@25.0.3) + '@inquirer/external-editor': 1.0.3(@types/node@25.0.9) '@manypkg/get-packages': 1.1.3 ansi-colors: 4.1.3 ci-info: 3.9.0 @@ -3409,34 +3430,34 @@ snapshots: dependencies: mime: 3.0.0 - '@cloudflare/unenv-preset@2.7.13(unenv@2.0.0-rc.24)(workerd@1.20251217.0)': + '@cloudflare/unenv-preset@2.9.0(unenv@2.0.0-rc.24)(workerd@1.20260111.0)': dependencies: unenv: 2.0.0-rc.24 optionalDependencies: - workerd: 1.20251217.0 + workerd: 1.20260111.0 - '@cloudflare/workerd-darwin-64@1.20251217.0': + '@cloudflare/workerd-darwin-64@1.20260111.0': optional: true - '@cloudflare/workerd-darwin-arm64@1.20251217.0': + '@cloudflare/workerd-darwin-arm64@1.20260111.0': optional: true - '@cloudflare/workerd-linux-64@1.20251217.0': + '@cloudflare/workerd-linux-64@1.20260111.0': optional: true - '@cloudflare/workerd-linux-arm64@1.20251217.0': + '@cloudflare/workerd-linux-arm64@1.20260111.0': optional: true - '@cloudflare/workerd-windows-64@1.20251217.0': + '@cloudflare/workerd-windows-64@1.20260111.0': optional: true '@cloudflare/workers-types@4.20251220.0': {} - '@commitlint/cli@20.2.0(@types/node@25.0.3)(typescript@5.9.3)': + '@commitlint/cli@20.2.0(@types/node@25.0.9)(typescript@5.9.3)': dependencies: '@commitlint/format': 20.2.0 '@commitlint/lint': 20.2.0 - '@commitlint/load': 20.2.0(@types/node@25.0.3)(typescript@5.9.3) + '@commitlint/load': 20.2.0(@types/node@25.0.9)(typescript@5.9.3) '@commitlint/read': 20.2.0 '@commitlint/types': 20.2.0 tinyexec: 1.0.2 @@ -3478,7 +3499,7 @@ snapshots: '@commitlint/rules': 20.2.0 '@commitlint/types': 20.2.0 - '@commitlint/load@20.2.0(@types/node@25.0.3)(typescript@5.9.3)': + '@commitlint/load@20.2.0(@types/node@25.0.9)(typescript@5.9.3)': dependencies: '@commitlint/config-validator': 20.2.0 '@commitlint/execute-rule': 20.0.0 @@ -3486,7 +3507,7 @@ snapshots: '@commitlint/types': 20.2.0 chalk: 5.6.2 cosmiconfig: 9.0.0(typescript@5.9.3) - cosmiconfig-typescript-loader: 6.2.0(@types/node@25.0.3)(cosmiconfig@9.0.0(typescript@5.9.3))(typescript@5.9.3) + cosmiconfig-typescript-loader: 6.2.0(@types/node@25.0.9)(cosmiconfig@9.0.0(typescript@5.9.3))(typescript@5.9.3) lodash.isplainobject: 4.0.6 lodash.merge: 4.6.2 lodash.uniq: 4.5.0 @@ -3625,7 +3646,7 @@ snapshots: dependencies: elysia: 1.4.19(@sinclair/typebox@0.34.38)(@types/bun@1.3.5)(exact-mirror@0.1.2(@sinclair/typebox@0.34.38))(file-type@21.0.0)(openapi-types@12.1.3)(typescript@5.9.3) - '@emnapi/runtime@1.7.1': + '@emnapi/runtime@1.8.1': dependencies: tslib: 2.8.1 optional: true @@ -3875,87 +3896,108 @@ snapshots: dependencies: '@gitmoji/gitmoji-regex': 1.0.0 - '@img/sharp-darwin-arm64@0.33.5': + '@img/colour@1.0.0': {} + + '@img/sharp-darwin-arm64@0.34.5': optionalDependencies: - '@img/sharp-libvips-darwin-arm64': 1.0.4 + '@img/sharp-libvips-darwin-arm64': 1.2.4 optional: true - '@img/sharp-darwin-x64@0.33.5': + '@img/sharp-darwin-x64@0.34.5': optionalDependencies: - '@img/sharp-libvips-darwin-x64': 1.0.4 + '@img/sharp-libvips-darwin-x64': 1.2.4 + optional: true + + '@img/sharp-libvips-darwin-arm64@1.2.4': optional: true - '@img/sharp-libvips-darwin-arm64@1.0.4': + '@img/sharp-libvips-darwin-x64@1.2.4': optional: true - '@img/sharp-libvips-darwin-x64@1.0.4': + '@img/sharp-libvips-linux-arm64@1.2.4': optional: true - '@img/sharp-libvips-linux-arm64@1.0.4': + '@img/sharp-libvips-linux-arm@1.2.4': optional: true - '@img/sharp-libvips-linux-arm@1.0.5': + '@img/sharp-libvips-linux-ppc64@1.2.4': optional: true - '@img/sharp-libvips-linux-s390x@1.0.4': + '@img/sharp-libvips-linux-riscv64@1.2.4': optional: true - '@img/sharp-libvips-linux-x64@1.0.4': + '@img/sharp-libvips-linux-s390x@1.2.4': optional: true - '@img/sharp-libvips-linuxmusl-arm64@1.0.4': + '@img/sharp-libvips-linux-x64@1.2.4': optional: true - '@img/sharp-libvips-linuxmusl-x64@1.0.4': + '@img/sharp-libvips-linuxmusl-arm64@1.2.4': optional: true - '@img/sharp-linux-arm64@0.33.5': + '@img/sharp-libvips-linuxmusl-x64@1.2.4': + optional: true + + '@img/sharp-linux-arm64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm64': 1.2.4 + optional: true + + '@img/sharp-linux-arm@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm': 1.2.4 + optional: true + + '@img/sharp-linux-ppc64@0.34.5': optionalDependencies: - '@img/sharp-libvips-linux-arm64': 1.0.4 + '@img/sharp-libvips-linux-ppc64': 1.2.4 optional: true - '@img/sharp-linux-arm@0.33.5': + '@img/sharp-linux-riscv64@0.34.5': optionalDependencies: - '@img/sharp-libvips-linux-arm': 1.0.5 + '@img/sharp-libvips-linux-riscv64': 1.2.4 optional: true - '@img/sharp-linux-s390x@0.33.5': + '@img/sharp-linux-s390x@0.34.5': optionalDependencies: - '@img/sharp-libvips-linux-s390x': 1.0.4 + '@img/sharp-libvips-linux-s390x': 1.2.4 optional: true - '@img/sharp-linux-x64@0.33.5': + '@img/sharp-linux-x64@0.34.5': optionalDependencies: - '@img/sharp-libvips-linux-x64': 1.0.4 + '@img/sharp-libvips-linux-x64': 1.2.4 optional: true - '@img/sharp-linuxmusl-arm64@0.33.5': + '@img/sharp-linuxmusl-arm64@0.34.5': optionalDependencies: - '@img/sharp-libvips-linuxmusl-arm64': 1.0.4 + '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 optional: true - '@img/sharp-linuxmusl-x64@0.33.5': + '@img/sharp-linuxmusl-x64@0.34.5': optionalDependencies: - '@img/sharp-libvips-linuxmusl-x64': 1.0.4 + '@img/sharp-libvips-linuxmusl-x64': 1.2.4 optional: true - '@img/sharp-wasm32@0.33.5': + '@img/sharp-wasm32@0.34.5': dependencies: - '@emnapi/runtime': 1.7.1 + '@emnapi/runtime': 1.8.1 + optional: true + + '@img/sharp-win32-arm64@0.34.5': optional: true - '@img/sharp-win32-ia32@0.33.5': + '@img/sharp-win32-ia32@0.34.5': optional: true - '@img/sharp-win32-x64@0.33.5': + '@img/sharp-win32-x64@0.34.5': optional: true - '@inquirer/external-editor@1.0.3(@types/node@25.0.3)': + '@inquirer/external-editor@1.0.3(@types/node@25.0.9)': dependencies: chardet: 2.1.1 iconv-lite: 0.7.1 optionalDependencies: - '@types/node': 25.0.3 + '@types/node': 25.0.9 '@inquirer/figures@1.0.13': {} @@ -4048,7 +4090,7 @@ snapshots: '@poppinss/dumper@0.6.5': dependencies: '@poppinss/colors': 4.1.6 - '@sindresorhus/is': 7.1.1 + '@sindresorhus/is': 7.2.0 supports-color: 10.2.2 '@poppinss/exception@1.2.3': {} @@ -4243,15 +4285,15 @@ snapshots: '@sinclair/typebox@0.34.38': {} - '@sindresorhus/is@7.1.1': {} + '@sindresorhus/is@7.2.0': {} - '@speed-highlight/core@1.2.12': {} + '@speed-highlight/core@1.2.14': {} '@tokenizer/inflate@0.2.7': dependencies: debug: 4.4.3 fflate: 0.8.2 - token-types: 6.1.1 + token-types: 6.1.2 transitivePeerDependencies: - supports-color @@ -4305,6 +4347,10 @@ snapshots: dependencies: undici-types: 7.16.0 + '@types/node@25.0.9': + dependencies: + undici-types: 7.16.0 + '@types/qs@6.14.0': {} '@types/range-parser@1.2.7': {} @@ -4441,7 +4487,7 @@ snapshots: bun-types@1.3.5: dependencies: - '@types/node': 25.0.3 + '@types/node': 25.0.9 optional: true callsites@3.1.0: {} @@ -4514,16 +4560,6 @@ snapshots: color-name@1.1.4: {} - color-string@1.9.1: - dependencies: - color-name: 1.1.4 - simple-swizzle: 0.2.4 - - color@4.2.3: - dependencies: - color-convert: 2.0.1 - color-string: 1.9.1 - commitlint-config-gitmoji@2.3.1: dependencies: '@commitlint/types': 17.8.1 @@ -4537,9 +4573,9 @@ snapshots: '@gitmoji/gitmoji-regex': 1.0.0 gitmojis: 3.15.0 - commitlint@20.2.0(@types/node@25.0.3)(typescript@5.9.3): + commitlint@20.2.0(@types/node@25.0.9)(typescript@5.9.3): dependencies: - '@commitlint/cli': 20.2.0(@types/node@25.0.3)(typescript@5.9.3) + '@commitlint/cli': 20.2.0(@types/node@25.0.9)(typescript@5.9.3) '@commitlint/types': 20.2.0 transitivePeerDependencies: - '@types/node' @@ -4589,9 +4625,9 @@ snapshots: cookie@1.1.1: {} - cosmiconfig-typescript-loader@6.2.0(@types/node@25.0.3)(cosmiconfig@9.0.0(typescript@5.9.3))(typescript@5.9.3): + cosmiconfig-typescript-loader@6.2.0(@types/node@25.0.9)(cosmiconfig@9.0.0(typescript@5.9.3))(typescript@5.9.3): dependencies: - '@types/node': 25.0.3 + '@types/node': 25.0.9 cosmiconfig: 9.0.0(typescript@5.9.3) jiti: 2.6.1 typescript: 5.9.3 @@ -4932,7 +4968,7 @@ snapshots: dependencies: '@tokenizer/inflate': 0.2.7 strtok3: 10.3.4 - token-types: 6.1.1 + token-types: 6.1.2 uint8array-extras: 1.5.0 transitivePeerDependencies: - supports-color @@ -5132,8 +5168,6 @@ snapshots: is-arrayish@0.2.1: {} - is-arrayish@0.3.4: {} - is-core-module@2.16.1: dependencies: hasown: 2.0.2 @@ -5338,20 +5372,20 @@ snapshots: mimic-function@5.0.1: {} - miniflare@4.20251217.0: + miniflare@4.20260111.0: dependencies: '@cspotcode/source-map-support': 0.8.1 acorn: 8.14.0 acorn-walk: 8.3.2 exit-hook: 2.2.1 glob-to-regexp: 0.4.1 - sharp: 0.33.5 + sharp: 0.34.5 stoppable: 1.1.0 undici: 7.14.0 - workerd: 1.20251217.0 + workerd: 1.20260111.0 ws: 8.18.0 youch: 4.1.0-beta.10 - zod: 3.22.3 + zod: 3.25.76 transitivePeerDependencies: - bufferutil - utf-8-validate @@ -5479,7 +5513,7 @@ snapshots: ky: 1.10.0 registry-auth-token: 5.1.0 registry-url: 6.0.1 - semver: 7.7.2 + semver: 7.7.3 package-manager-detector@0.2.11: dependencies: @@ -5694,31 +5728,36 @@ snapshots: dependencies: type-fest: 0.20.2 - sharp@0.33.5: + sharp@0.34.5: dependencies: - color: 4.2.3 + '@img/colour': 1.0.0 detect-libc: 2.1.2 semver: 7.7.3 optionalDependencies: - '@img/sharp-darwin-arm64': 0.33.5 - '@img/sharp-darwin-x64': 0.33.5 - '@img/sharp-libvips-darwin-arm64': 1.0.4 - '@img/sharp-libvips-darwin-x64': 1.0.4 - '@img/sharp-libvips-linux-arm': 1.0.5 - '@img/sharp-libvips-linux-arm64': 1.0.4 - '@img/sharp-libvips-linux-s390x': 1.0.4 - '@img/sharp-libvips-linux-x64': 1.0.4 - '@img/sharp-libvips-linuxmusl-arm64': 1.0.4 - '@img/sharp-libvips-linuxmusl-x64': 1.0.4 - '@img/sharp-linux-arm': 0.33.5 - '@img/sharp-linux-arm64': 0.33.5 - '@img/sharp-linux-s390x': 0.33.5 - '@img/sharp-linux-x64': 0.33.5 - '@img/sharp-linuxmusl-arm64': 0.33.5 - '@img/sharp-linuxmusl-x64': 0.33.5 - '@img/sharp-wasm32': 0.33.5 - '@img/sharp-win32-ia32': 0.33.5 - '@img/sharp-win32-x64': 0.33.5 + '@img/sharp-darwin-arm64': 0.34.5 + '@img/sharp-darwin-x64': 0.34.5 + '@img/sharp-libvips-darwin-arm64': 1.2.4 + '@img/sharp-libvips-darwin-x64': 1.2.4 + '@img/sharp-libvips-linux-arm': 1.2.4 + '@img/sharp-libvips-linux-arm64': 1.2.4 + '@img/sharp-libvips-linux-ppc64': 1.2.4 + '@img/sharp-libvips-linux-riscv64': 1.2.4 + '@img/sharp-libvips-linux-s390x': 1.2.4 + '@img/sharp-libvips-linux-x64': 1.2.4 + '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 + '@img/sharp-libvips-linuxmusl-x64': 1.2.4 + '@img/sharp-linux-arm': 0.34.5 + '@img/sharp-linux-arm64': 0.34.5 + '@img/sharp-linux-ppc64': 0.34.5 + '@img/sharp-linux-riscv64': 0.34.5 + '@img/sharp-linux-s390x': 0.34.5 + '@img/sharp-linux-x64': 0.34.5 + '@img/sharp-linuxmusl-arm64': 0.34.5 + '@img/sharp-linuxmusl-x64': 0.34.5 + '@img/sharp-wasm32': 0.34.5 + '@img/sharp-win32-arm64': 0.34.5 + '@img/sharp-win32-ia32': 0.34.5 + '@img/sharp-win32-x64': 0.34.5 shebang-command@2.0.0: dependencies: @@ -5730,10 +5769,6 @@ snapshots: signal-exit@4.1.0: {} - simple-swizzle@0.2.4: - dependencies: - is-arrayish: 0.3.4 - slash@3.0.0: {} smart-buffer@4.2.0: {} @@ -5829,9 +5864,9 @@ snapshots: dependencies: is-number: 7.0.0 - token-types@6.1.1: + token-types@6.1.2: dependencies: - '@borewit/text-codec': 0.1.1 + '@borewit/text-codec': 0.2.1 '@tokenizer/token': 0.3.0 ieee754: 1.2.1 @@ -5945,24 +5980,24 @@ snapshots: dependencies: string-width: 7.2.0 - workerd@1.20251217.0: + workerd@1.20260111.0: optionalDependencies: - '@cloudflare/workerd-darwin-64': 1.20251217.0 - '@cloudflare/workerd-darwin-arm64': 1.20251217.0 - '@cloudflare/workerd-linux-64': 1.20251217.0 - '@cloudflare/workerd-linux-arm64': 1.20251217.0 - '@cloudflare/workerd-windows-64': 1.20251217.0 + '@cloudflare/workerd-darwin-64': 1.20260111.0 + '@cloudflare/workerd-darwin-arm64': 1.20260111.0 + '@cloudflare/workerd-linux-64': 1.20260111.0 + '@cloudflare/workerd-linux-arm64': 1.20260111.0 + '@cloudflare/workerd-windows-64': 1.20260111.0 - wrangler@4.56.0(@cloudflare/workers-types@4.20251220.0): + wrangler@4.59.1(@cloudflare/workers-types@4.20251220.0): dependencies: '@cloudflare/kv-asset-handler': 0.4.1 - '@cloudflare/unenv-preset': 2.7.13(unenv@2.0.0-rc.24)(workerd@1.20251217.0) + '@cloudflare/unenv-preset': 2.9.0(unenv@2.0.0-rc.24)(workerd@1.20260111.0) blake3-wasm: 2.1.5 esbuild: 0.27.0 - miniflare: 4.20251217.0 + miniflare: 4.20260111.0 path-to-regexp: 6.3.0 unenv: 2.0.0-rc.24 - workerd: 1.20251217.0 + workerd: 1.20260111.0 optionalDependencies: '@cloudflare/workers-types': 4.20251220.0 fsevents: 2.3.3 @@ -6023,8 +6058,8 @@ snapshots: dependencies: '@poppinss/colors': 4.1.6 '@poppinss/dumper': 0.6.5 - '@speed-highlight/core': 1.2.12 + '@speed-highlight/core': 1.2.14 cookie: 1.1.1 youch-core: 0.3.3 - zod@3.22.3: {} + zod@3.25.76: {} From e1987c31ac072fbccdf258c6856e869fc7ad54f1 Mon Sep 17 00:00:00 2001 From: ItsRauf <31735267+ItsRauf@users.noreply.github.com> Date: Sat, 7 Feb 2026 14:40:09 -0600 Subject: [PATCH 03/20] =?UTF-8?q?=E2=9A=B0=EF=B8=8F=20chore(workspace):=20?= =?UTF-8?q?remove=20stupid=20comments?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/api/worker-configuration.d.ts | 10 +++++---- packages/builder/src/Embed.ts | 33 ++--------------------------- packages/logging/src/main.ts | 2 -- packages/platforms/src/Instagram.ts | 1 - packages/platforms/src/Platform.ts | 8 ------- packages/platforms/src/Reddit.ts | 1 - packages/platforms/src/Threads.ts | 1 - packages/platforms/src/TikTok.ts | 2 -- packages/platforms/src/constants.ts | 2 -- 9 files changed, 8 insertions(+), 52 deletions(-) diff --git a/apps/api/worker-configuration.d.ts b/apps/api/worker-configuration.d.ts index b0cdaf1..130b2d2 100644 --- a/apps/api/worker-configuration.d.ts +++ b/apps/api/worker-configuration.d.ts @@ -1,6 +1,6 @@ /* eslint-disable */ // Generated by Wrangler by running `wrangler types` (hash: 37f49ce2d8d1275f19bca443420efd84) -// Runtime types generated with workerd@1.20251217.0 2025-06-23 nodejs_compat +// Runtime types generated with workerd@1.20260111.0 2025-06-23 nodejs_compat declare namespace Cloudflare { interface GlobalProps { mainModule: typeof import("./src/main"); @@ -507,8 +507,10 @@ interface DurableObjectNamespaceNewUniqueIdOptions { jurisdiction?: DurableObjectJurisdiction; } type DurableObjectLocationHint = "wnam" | "enam" | "sam" | "weur" | "eeur" | "apac" | "oc" | "afr" | "me"; +type DurableObjectRoutingMode = "primary-only"; interface DurableObjectNamespaceGetDurableObjectOptions { locationHint?: DurableObjectLocationHint; + routingMode?: DurableObjectRoutingMode; } interface DurableObjectClass<_T extends Rpc.DurableObjectBranded | undefined = undefined> { } @@ -2092,6 +2094,8 @@ interface Transformer { expectedLength?: number; } interface StreamPipeOptions { + preventAbort?: boolean; + preventCancel?: boolean; /** * Pipes this readable stream to a given writable stream destination. The way in which the piping process behaves under various error conditions can be customized with a number of passed options. It returns a promise that fulfills when the piping process completes successfully, or rejects if any errors were encountered. * @@ -2110,8 +2114,6 @@ interface StreamPipeOptions { * The signal option can be set to an AbortSignal to allow aborting an ongoing pipe operation via the corresponding AbortController. In this case, this source readable stream will be canceled, and destination aborted, unless the respective options preventCancel or preventAbort are set. */ preventClose?: boolean; - preventAbort?: boolean; - preventCancel?: boolean; signal?: AbortSignal; } type ReadableStreamReadResult = { @@ -2386,13 +2388,13 @@ declare abstract class TransformStreamDefaultController { terminate(): void; } interface ReadableWritablePair { + readable: ReadableStream; /** * Provides a convenient, chainable way of piping this readable stream through a transform stream (or any other { writable, readable } pair). It simply pipes the stream into the writable side of the supplied pair, and returns the readable side for further use. * * Piping a stream will lock it for the duration of the pipe, preventing any other consumer from acquiring a reader. */ writable: WritableStream; - readable: ReadableStream; } /** * The **`WritableStream`** interface of the Streams API provides a standard abstraction for writing streaming data to a destination, known as a sink. diff --git a/packages/builder/src/Embed.ts b/packages/builder/src/Embed.ts index 8f6d793..3d31b50 100644 --- a/packages/builder/src/Embed.ts +++ b/packages/builder/src/Embed.ts @@ -15,14 +15,9 @@ import { import { type APIMediaGalleryItem, - ButtonStyle, SeparatorSpacingSize } from "discord-api-types/v10"; -// ============================================================================ -// Types - moved from @embedly/types -// ============================================================================ - export interface StatsData { comments: number; reposts?: number; @@ -31,7 +26,6 @@ export interface StatsData { views?: number; } -// Stat emojis (not platform-specific) export const statEmojis = { comments: "<:comment:1386639521373753374>", reposts: "<:repost:1386639564143198349>", @@ -352,33 +346,10 @@ export class Embed implements EmbedData { private static addFooterSection( container: ContainerBuilder, embed: Embed, - link_style: EmbedFlags[EmbedFlagNames.LinkStyle] + _link_style?: EmbedFlags[EmbedFlagNames.LinkStyle] ) { const stats = Embed.formatStats(embed); - if (link_style === "control") { - container.addSectionComponents((builder) => { - builder.addTextDisplayComponents((builder) => - builder.setContent( - `${stats.length > 0 ? `${subtext(stats.join(" "))}\n` : ""}${embed.emoji} • ${time( - embed.timestamp, - TimestampStyles.LongDateShortTime - )}` - ) - ); - - builder.setButtonAccessory((builder) => - builder - .setStyle(ButtonStyle.Link) - .setURL(embed.url) - .setLabel(`View on ${embed.platform}`) - ); - - return builder; - }); - return; - } - container.addSeparatorComponents((builder) => builder.setDivider(true).setSpacing(SeparatorSpacingSize.Large) ); @@ -388,7 +359,7 @@ export class Embed implements EmbedData { `${stats.length > 0 ? `${subtext(stats.join(" "))}\n` : ""}${embed.emoji} • ${time( embed.timestamp, TimestampStyles.LongDateShortTime - )} • ${link_style === "inline" ? hyperlink(`View on ${embed.platform}`, embed.url) : ""}` + )} • ${hyperlink(`View on ${embed.platform}`, embed.url)}` ) ); } diff --git a/packages/logging/src/main.ts b/packages/logging/src/main.ts index 948b9c0..219a74f 100644 --- a/packages/logging/src/main.ts +++ b/packages/logging/src/main.ts @@ -1,5 +1,3 @@ -// Platform type is just a string for logging purposes -// This avoids circular dependency with @embedly/types type PlatformName = string; export interface EmbedlyLogBase { diff --git a/packages/platforms/src/Instagram.ts b/packages/platforms/src/Instagram.ts index a767ab3..2551dd4 100644 --- a/packages/platforms/src/Instagram.ts +++ b/packages/platforms/src/Instagram.ts @@ -128,7 +128,6 @@ export class Instagram extends EmbedlyPlatform { createEmbed(post_data: any): Embed { const embed = new Embed(this.transformRawData(post_data)); const media = this.parsePostMedia(post_data); - // Media truncation will be handled by Embed.setMedia embed.setMedia(media); return embed; diff --git a/packages/platforms/src/Platform.ts b/packages/platforms/src/Platform.ts index 51a083d..71010a7 100644 --- a/packages/platforms/src/Platform.ts +++ b/packages/platforms/src/Platform.ts @@ -8,7 +8,6 @@ import { } from "@embedly/logging"; import type { EmbedlyPlatformType } from "./types.ts"; -// Re-export for convenience export type { BaseEmbedData } from "@embedly/builder"; export interface EmbedlyPlatformLogMessages { @@ -28,28 +27,22 @@ export interface CloudflareEnv { } export abstract class EmbedlyPlatform { - // Config properties - each platform must define these abstract readonly color: readonly [number, number, number]; abstract readonly emoji: string; abstract readonly regex: RegExp; - // Auto-generated log messages public log_messages: EmbedlyPlatformLogMessages; constructor( public name: EmbedlyPlatformType, private cache_prefix: string ) { - // Auto-generate log messages from platform name this.log_messages = { fetching: EMBEDLY_FETCH_PLATFORM(name), failed: EMBEDLY_FAILED_PLATFORM(name) }; } - /** - * Test if a URL matches this platform's regex - */ public matchesUrl(url: string): boolean { return this.regex.test(url); } @@ -78,7 +71,6 @@ export abstract class EmbedlyPlatform { }); } - // env is optional and partial - each platform uses what it needs abstract fetchPost( post_id: string, env?: Partial diff --git a/packages/platforms/src/Reddit.ts b/packages/platforms/src/Reddit.ts index a13b8fe..d454847 100644 --- a/packages/platforms/src/Reddit.ts +++ b/packages/platforms/src/Reddit.ts @@ -145,7 +145,6 @@ export class Reddit extends EmbedlyPlatform { const embed = new Embed(data); - // Media truncation will be handled by Embed.setMedia if (media.length > 0) { embed.setMedia(media); } diff --git a/packages/platforms/src/Threads.ts b/packages/platforms/src/Threads.ts index 07f6ab3..2389026 100644 --- a/packages/platforms/src/Threads.ts +++ b/packages/platforms/src/Threads.ts @@ -144,7 +144,6 @@ export class Threads extends EmbedlyPlatform { const embed = new Embed(this.transformRawData(post_data)); const media = this.parsePostMedia(post_data); if (media.length > 0) { - // Media truncation will be handled by Embed.setMedia embed.setMedia(media); } diff --git a/packages/platforms/src/TikTok.ts b/packages/platforms/src/TikTok.ts index d3463ae..cd997c5 100644 --- a/packages/platforms/src/TikTok.ts +++ b/packages/platforms/src/TikTok.ts @@ -8,10 +8,8 @@ import { } from "./Platform.ts"; import { EmbedlyPlatformType } from "./types.ts"; -// Main regex for initial URL detection const TIKTOK_REGEX_MAIN = /(https?:\/\/)?(?:[\w-]+\.)*tiktok\.com/; -// Detailed regex for extracting user and video ID from expanded URL const TIKTOK_REGEX_DETAIL = /https:\/\/(?:m|www|vm)?\.?tiktok\.com\/(?@[\w.-]+)\/video\/(?\d+)/; diff --git a/packages/platforms/src/constants.ts b/packages/platforms/src/constants.ts index a68236c..7ad4ae8 100644 --- a/packages/platforms/src/constants.ts +++ b/packages/platforms/src/constants.ts @@ -1,4 +1,3 @@ -// Cloudflare cache options for platform API calls export const CF_CACHE_OPTIONS = { cf: { cacheTtl: 60 * 60 * 24, @@ -6,6 +5,5 @@ export const CF_CACHE_OPTIONS = { } } as const; -// Generic link regex for detecting URLs export const GENERIC_LINK_REGEX = /\b((?:[a-z][\w-]+:(?:\/{1,3}|[a-z0-9%])|www\d{0,3}[.]|[a-z0-9.-]+[.][a-z]{2,4}\/)(?:[^\s()<>]+|\(([^\s()<>]+|(\([^\s()<>]+\)))*\))+(?:\(([^\s()<>]+|(\([^\s()<>]+\)))*\)|[^\s`!()[\]{};:'".,<>?«»""'']))/i; From d7484b00d8d34fe5e4d50bcae2ab89e6ee1e41a8 Mon Sep 17 00:00:00 2001 From: ItsRauf <31735267+ItsRauf@users.noreply.github.com> Date: Tue, 10 Feb 2026 21:25:26 -0600 Subject: [PATCH 04/20] =?UTF-8?q?=F0=9F=A5=85=20fix(platforms,api):=20hard?= =?UTF-8?q?en=20error=20handling=20and=20type=20safety?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/api/src/main.ts | 22 +++++++++- packages/platforms/src/CBC.ts | 45 ++++++++++++++++---- packages/platforms/src/Instagram.ts | 22 ++++++++-- packages/platforms/src/Platform.ts | 2 +- packages/platforms/src/Reddit.ts | 41 ++++++++++++++---- packages/platforms/src/Threads.ts | 22 ++++++++-- packages/platforms/src/TikTok.ts | 64 +++++++++++++++++++++-------- packages/platforms/src/Twitter.ts | 33 ++++++++++++--- packages/platforms/src/main.ts | 14 +++---- packages/platforms/src/parsing.ts | 42 ------------------- packages/platforms/src/utils.ts | 26 ++++++++++++ 11 files changed, 236 insertions(+), 97 deletions(-) delete mode 100644 packages/platforms/src/parsing.ts create mode 100644 packages/platforms/src/utils.ts diff --git a/apps/api/src/main.ts b/apps/api/src/main.ts index cb53f4a..c7d21a5 100644 --- a/apps/api/src/main.ts +++ b/apps/api/src/main.ts @@ -56,7 +56,25 @@ const app = (env: Env, ctx: ExecutionContext) => "/api/scrape", async ({ body: { platform, url }, status, logger, set }) => { const handler = Platforms[platform as keyof typeof Platforms]; - const post_id = await handler.parsePostId(url); + + let post_id: string; + try { + post_id = await handler.parsePostId(url); + } catch (error: any) { + logger.error("Failed to parse post ID", { + platform: handler.name, + url, + error: error.message || String(error) + }); + + return status(400, { + type: "EMBEDLY_INVALID_URL", + status: 400, + title: "Invalid URL", + detail: `Could not extract post ID from ${platform} URL` + }); + } + const post_log_ctx: EmbedlyPostContext = { platform: handler.name, post_url: url, @@ -97,7 +115,7 @@ const app = (env: Env, ctx: ExecutionContext) => post_data!.url = url; } - handler.addPostToCache(post_id, post_data, env.STORAGE); + await handler.addPostToCache(post_id, post_data, env.STORAGE); logger.debug( ...formatBetterStack(EMBEDLY_CACHING_POST, post_log_ctx) ); diff --git a/packages/platforms/src/CBC.ts b/packages/platforms/src/CBC.ts index feab2dd..8473e1d 100644 --- a/packages/platforms/src/CBC.ts +++ b/packages/platforms/src/CBC.ts @@ -5,6 +5,7 @@ import he from "he"; import { CF_CACHE_OPTIONS } from "./constants.ts"; import { type BaseEmbedData, EmbedlyPlatform } from "./Platform.ts"; import { EmbedlyPlatformType } from "./types.ts"; +import { validateRegexMatch } from "./utils.ts"; export class CBC extends EmbedlyPlatform { readonly color = [215, 36, 42] as const; @@ -16,8 +17,9 @@ export class CBC extends EmbedlyPlatform { } async parsePostId(url: string): Promise { - const match = this.regex.exec(url)!; - const { cbc_id } = match.groups!; + const match = this.regex.exec(url); + validateRegexMatch(match, "Invalid CBC URL: could not extract ID"); + const { cbc_id } = match.groups; return cbc_id; } @@ -27,19 +29,44 @@ export class CBC extends EmbedlyPlatform { redirect: "follow", ...CF_CACHE_OPTIONS }); + if (!resp.ok) { throw { code: resp.status, message: resp.statusText }; } + const html = await resp.text(); const $ = cheerio.load(html); const script = $("script#initialStateDom"); - const data = JSON.parse( - script - .text() - .replace("window.__INITIAL_STATE__ = ", "") - .slice(0, -1) - ); - return data.app.meta.jsonld; + const scriptText = script.text(); + + if (!scriptText) { + throw { + code: 500, + message: "CBC page structure changed: missing data" + }; + } + + let data: any; + try { + data = JSON.parse( + scriptText + .replace("window.__INITIAL_STATE__ = ", "") + .slice(0, -1) + ); + } catch { + throw { code: 500, message: "Failed to parse CBC data" }; + } + + const jsonld = data?.app?.meta?.jsonld; + + if (!jsonld) { + throw { + code: 500, + message: "CBC page structure changed: missing metadata" + }; + } + + return jsonld; } transformRawData(raw_data: any): BaseEmbedData { diff --git a/packages/platforms/src/Instagram.ts b/packages/platforms/src/Instagram.ts index 2551dd4..63ce935 100644 --- a/packages/platforms/src/Instagram.ts +++ b/packages/platforms/src/Instagram.ts @@ -6,6 +6,7 @@ import { EmbedlyPlatform } from "./Platform.ts"; import { EmbedlyPlatformType } from "./types.ts"; +import { validateRegexMatch } from "./utils.ts"; export class Instagram extends EmbedlyPlatform { readonly color = [225, 48, 108] as const; @@ -27,8 +28,12 @@ export class Instagram extends EmbedlyPlatform { }); url = req.url; } - const match = this.regex.exec(url)!; - const { ig_shortcode } = match.groups!; + const match = this.regex.exec(url); + validateRegexMatch( + match, + "Invalid Instagram URL: could not extract shortcode" + ); + const { ig_shortcode } = match.groups; return ig_shortcode; } @@ -56,11 +61,22 @@ export class Instagram extends EmbedlyPlatform { }, ...CF_CACHE_OPTIONS }); + if (!resp.ok) { throw { code: resp.status, message: resp.statusText }; } + const { data } = (await resp.json()) as Record; - return data.xdt_shortcode_media; + const media = data?.xdt_shortcode_media; + + if (!media) { + throw { + code: 500, + message: "Instagram API returned unexpected structure" + }; + } + + return media; } parsePostMedia( diff --git a/packages/platforms/src/Platform.ts b/packages/platforms/src/Platform.ts index 71010a7..12e7da7 100644 --- a/packages/platforms/src/Platform.ts +++ b/packages/platforms/src/Platform.ts @@ -66,7 +66,7 @@ export abstract class EmbedlyPlatform { cache_store: KVNamespace ): Promise { const cache_key = `${this.cache_prefix}:${post_id}`; - cache_store.put(cache_key, JSON.stringify(post_data), { + await cache_store.put(cache_key, JSON.stringify(post_data), { expirationTtl: 60 * 60 * 24 }); } diff --git a/packages/platforms/src/Reddit.ts b/packages/platforms/src/Reddit.ts index d454847..0cd1572 100644 --- a/packages/platforms/src/Reddit.ts +++ b/packages/platforms/src/Reddit.ts @@ -6,6 +6,7 @@ import { EmbedlyPlatform } from "./Platform.ts"; import { EmbedlyPlatformType } from "./types.ts"; +import { validateRegexMatch } from "./utils.ts"; export class Reddit extends EmbedlyPlatform { readonly color = [255, 86, 0] as const; @@ -18,8 +19,12 @@ export class Reddit extends EmbedlyPlatform { } async parsePostId(url: string): Promise { - const match = this.regex.exec(url)!; - const { post_id, subreddit } = match.groups!; + const match = this.regex.exec(url); + validateRegexMatch( + match, + "Invalid Reddit URL: could not extract post ID or subreddit" + ); + const { post_id, subreddit } = match.groups; return `${subreddit}/${post_id}`; } @@ -45,8 +50,26 @@ export class Reddit extends EmbedlyPlatform { const post_data = (await resp.json()) as Record; + const postDataItem = post_data?.[0]?.data?.children?.[0]?.data; + + if (!postDataItem) { + throw { + code: 500, + message: "Reddit API returned unexpected structure" + }; + } + + const authorName = postDataItem.author; + + if (!authorName) { + throw { + code: 500, + message: "Reddit post missing author information" + }; + } + const profile_resp = await fetch( - `https://www.reddit.com/user/${post_data[0].data.children[0].data.author}/about.json?raw_json=1`, + `https://www.reddit.com/user/${authorName}/about.json?raw_json=1`, { method: "GET", headers: { @@ -66,10 +89,14 @@ export class Reddit extends EmbedlyPlatform { const { data: profile_data } = (await profile_resp.json()) as Record; - return { - post_data: post_data[0].data.children[0].data, - profile_data - }; + if (!profile_data) { + throw { + code: 500, + message: "Reddit profile API returned unexpected structure" + }; + } + + return { post_data: postDataItem, profile_data }; } parsePostMedia( diff --git a/packages/platforms/src/Threads.ts b/packages/platforms/src/Threads.ts index 2389026..c2f7d8e 100644 --- a/packages/platforms/src/Threads.ts +++ b/packages/platforms/src/Threads.ts @@ -5,6 +5,7 @@ import { EmbedlyPlatform } from "./Platform.ts"; import { EmbedlyPlatformType } from "./types.ts"; +import { validateRegexMatch } from "./utils.ts"; const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"; @@ -20,8 +21,12 @@ export class Threads extends EmbedlyPlatform { } async parsePostId(url: string): Promise { - const match = this.regex.exec(url)!; - const { thread_shortcode } = match.groups!; + const match = this.regex.exec(url); + validateRegexMatch( + match, + "Invalid Threads URL: could not extract shortcode" + ); + const { thread_shortcode } = match.groups; const thread_id = thread_shortcode .split("") .reduce( @@ -80,11 +85,22 @@ export class Threads extends EmbedlyPlatform { "Sec-Fetch-Site": "same-origin" } }); + if (!resp.ok) { throw { code: resp.status, message: resp.statusText }; } + const { data } = (await resp.json()) as Record; - return data.data.edges[0].node.thread_items[0].post; + const post = data?.data?.edges?.[0]?.node?.thread_items?.[0]?.post; + + if (!post) { + throw { + code: 500, + message: "Threads API returned unexpected structure" + }; + } + + return post; } parsePostMedia( diff --git a/packages/platforms/src/TikTok.ts b/packages/platforms/src/TikTok.ts index cd997c5..855e6e4 100644 --- a/packages/platforms/src/TikTok.ts +++ b/packages/platforms/src/TikTok.ts @@ -7,6 +7,7 @@ import { EmbedlyPlatform } from "./Platform.ts"; import { EmbedlyPlatformType } from "./types.ts"; +import { validateRegexMatch } from "./utils.ts"; const TIKTOK_REGEX_MAIN = /(https?:\/\/)?(?:[\w-]+\.)*tiktok\.com/; @@ -24,8 +25,12 @@ export class TikTok extends EmbedlyPlatform { async parsePostId(url: string): Promise { const req = await fetch(url, { redirect: "follow" }); - const match = TIKTOK_REGEX_DETAIL.exec(req.url)!; - const { tiktok_user, tiktok_id } = match.groups!; + const match = TIKTOK_REGEX_DETAIL.exec(req.url); + validateRegexMatch( + match, + "Invalid TikTok URL: could not extract user/id" + ); + const { tiktok_user, tiktok_id } = match.groups; return `${tiktok_user}/${tiktok_id}`; } @@ -44,15 +49,41 @@ export class TikTok extends EmbedlyPlatform { ...CF_CACHE_OPTIONS } ); + if (!resp.ok) { throw { code: resp.status, message: resp.statusText }; } + const html = await resp.text(); const $ = cheerio.load(html); const script = $("script#__UNIVERSAL_DATA_FOR_REHYDRATION__"); - const data = JSON.parse(script.text()); - return data.__DEFAULT_SCOPE__["webapp.video-detail"].itemInfo - .itemStruct; + const scriptText = script.text(); + + if (!scriptText) { + throw { + code: 500, + message: "TikTok page structure changed: missing data script" + }; + } + + let data: any; + try { + data = JSON.parse(scriptText); + } catch { + throw { code: 500, message: "Failed to parse TikTok data" }; + } + const itemStruct = + data?.__DEFAULT_SCOPE__?.["webapp.video-detail"]?.itemInfo + ?.itemStruct; + + if (!itemStruct) { + throw { + code: 500, + message: "TikTok page structure changed: missing video data" + }; + } + + return itemStruct; } transformRawData(raw_data: any): BaseEmbedData { @@ -78,17 +109,18 @@ export class TikTok extends EmbedlyPlatform { async createEmbed(post_data: any): Promise { const embed = new Embed(this.transformRawData(post_data)); - const video = await fetch( - post_data.video.bitrateInfo[0].PlayAddr.UrlList[2], - { redirect: "follow" } - ); - embed.setMedia([ - { - media: { - url: video.url - } - } - ]); + + const videoUrl = + post_data?.video?.bitrateInfo?.[0]?.PlayAddr?.UrlList?.[2]; + + if (!videoUrl) { + return embed; + } + + try { + const video = await fetch(videoUrl, { redirect: "follow" }); + embed.setMedia([{ media: { url: video.url } }]); + } catch {} return embed; } diff --git a/packages/platforms/src/Twitter.ts b/packages/platforms/src/Twitter.ts index 183db79..07f01eb 100644 --- a/packages/platforms/src/Twitter.ts +++ b/packages/platforms/src/Twitter.ts @@ -3,6 +3,7 @@ import he from "he"; import { CF_CACHE_OPTIONS } from "./constants.ts"; import { type BaseEmbedData, EmbedlyPlatform } from "./Platform.ts"; import { EmbedlyPlatformType } from "./types.ts"; +import { validateRegexMatch } from "./utils.ts"; export class Twitter extends EmbedlyPlatform { readonly color = [29, 161, 242] as const; @@ -15,14 +16,18 @@ export class Twitter extends EmbedlyPlatform { } async parsePostId(url: string): Promise { - const match = this.regex.exec(url)!; - const { tweet_id } = match.groups!; + const match = this.regex.exec(url); + validateRegexMatch( + match, + "Invalid Twitter URL: could not extract tweet ID" + ); + const { tweet_id } = match.groups; return tweet_id; } async fetchPost(tweet_id: string): Promise { - const { tweet, code, message } = await fetch( - `https://api.fxtwitter.com/embedly/status/${tweet_id}`, + const resp = await fetch( + `https://api.fxtwitter.com/embedly/status/${tweet_id}/en`, { method: "GET", headers: { @@ -30,7 +35,17 @@ export class Twitter extends EmbedlyPlatform { }, ...CF_CACHE_OPTIONS } - ).then((r) => r.json() as Record); + ); + + if (!resp.ok) { + throw { code: resp.status, message: resp.statusText }; + } + + const { tweet, code, message } = (await resp.json()) as Record< + string, + any + >; + if (code !== 200) { throw { code, message }; } @@ -40,6 +55,11 @@ export class Twitter extends EmbedlyPlatform { enrichTweetText(text_data: Record) { let text = text_data.text as string; + + if (!text_data.facets || !Array.isArray(text_data.facets)) { + return he.decode(text); + } + for (const facet of text_data.facets) { if (facet.type === "url") { text = text.replace(facet.original, facet.replacement); @@ -83,6 +103,9 @@ export class Twitter extends EmbedlyPlatform { if (tweet_data.text !== "") { embed.setDescription(this.enrichTweetText(tweet_data.raw_text)); } + if (tweet_data.translation?.text) { + embed.setDescription(tweet_data.translation.text); + } if (tweet_data.media) { embed.setMedia( tweet_data.media.all.map((media: any) => ({ diff --git a/packages/platforms/src/main.ts b/packages/platforms/src/main.ts index 83c4389..807a94b 100644 --- a/packages/platforms/src/main.ts +++ b/packages/platforms/src/main.ts @@ -1,6 +1,5 @@ import { CBC } from "./CBC.ts"; import { Instagram } from "./Instagram.ts"; -import { registerPlatformDetector } from "./parsing.ts"; import { Reddit } from "./Reddit.ts"; import { Threads } from "./Threads.ts"; import { TikTok } from "./TikTok.ts"; @@ -30,23 +29,19 @@ export const platformEmojis = Object.fromEntries( ]) ) as Record; -registerPlatformDetector((url: string) => { +export function getPlatformFromURL( + url: string +): null | { type: EmbedlyPlatformType } { for (const [name, platform] of Object.entries(Platforms)) { if (platform.matchesUrl(url)) { return { type: name as EmbedlyPlatformType }; } } return null; -}); +} export { CF_CACHE_OPTIONS, GENERIC_LINK_REGEX } from "./constants.ts"; export { type CloudflareEnv, EmbedlyPlatform } from "./Platform.ts"; -export { - getPlatformFromURL, - hasLink, - isEscaped, - isSpoiler -} from "./parsing.ts"; export { EmbedlyPlatformType, type Emojis, @@ -54,5 +49,6 @@ export { type StatEmojis, statEmojis } from "./types.ts"; +export { hasLink, isEscaped, isSpoiler } from "./utils.ts"; export default Platforms; diff --git a/packages/platforms/src/parsing.ts b/packages/platforms/src/parsing.ts deleted file mode 100644 index 2c77830..0000000 --- a/packages/platforms/src/parsing.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { GENERIC_LINK_REGEX } from "./constants.ts"; -import type { EmbedlyPlatformType } from "./types.ts"; - -/** - * Check if content contains any link - */ -export function hasLink(content: string): boolean { - return GENERIC_LINK_REGEX.test(content); -} - -/** - * Check if a URL is wrapped in spoiler tags (||url||) - */ -export function isSpoiler(url: string, content: string): boolean { - return content - .split("||") - .some((part, ind) => ind % 2 === 1 && part.includes(url)); -} - -/** - * Check if a URL is escaped with angle brackets () - */ -export function isEscaped(url: string, content: string): boolean { - return content.includes(`<${url}>`); -} - -/** - * Data-driven platform detection - will be populated by main.ts - * after platform instances are created - */ -export let getPlatformFromURL: ( - url: string -) => null | { type: EmbedlyPlatformType }; - -/** - * Register the platform detection function (called from main.ts) - */ -export function registerPlatformDetector( - detector: (url: string) => null | { type: EmbedlyPlatformType } -): void { - getPlatformFromURL = detector; -} diff --git a/packages/platforms/src/utils.ts b/packages/platforms/src/utils.ts new file mode 100644 index 0000000..ed24efb --- /dev/null +++ b/packages/platforms/src/utils.ts @@ -0,0 +1,26 @@ +import { GENERIC_LINK_REGEX } from "./constants.ts"; + +export function hasLink(content: string): boolean { + return GENERIC_LINK_REGEX.test(content); +} + +export function isSpoiler(url: string, content: string): boolean { + return content + .split("||") + .some((part, ind) => ind % 2 === 1 && part.includes(url)); +} + +export function isEscaped(url: string, content: string): boolean { + return content.includes(`<${url}>`); +} + +export function validateRegexMatch( + match: RegExpExecArray | null, + errorMessage?: string +): asserts match is RegExpExecArray & { + groups: Record; +} { + if (match === null || match.groups === undefined) { + throw new Error(errorMessage ?? "Invalid URL: regex match failed"); + } +} From dff6d144c0b087a40fcafda4d15a71d602bd067d Mon Sep 17 00:00:00 2001 From: ItsRauf <31735267+ItsRauf@users.noreply.github.com> Date: Tue, 10 Feb 2026 21:28:29 -0600 Subject: [PATCH 05/20] =?UTF-8?q?=E2=9C=A8=20feat(bot):=20add=20messageDel?= =?UTF-8?q?ete=20listener=20to=20auto-remove=20orphaned=20embeds?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/bot/src/client.ts | 4 +++ apps/bot/src/commands/delete.ts | 36 ++++++++++++++++--- apps/bot/src/commands/embed.ts | 17 ++++++--- apps/bot/src/listeners/messageCreate.ts | 21 ++++++++--- apps/bot/src/listeners/messageDelete.ts | 46 +++++++++++++++++++++++++ packages/logging/src/main.ts | 8 +++++ 6 files changed, 118 insertions(+), 14 deletions(-) create mode 100644 apps/bot/src/listeners/messageDelete.ts diff --git a/apps/bot/src/client.ts b/apps/bot/src/client.ts index f089c34..8a0a0c4 100644 --- a/apps/bot/src/client.ts +++ b/apps/bot/src/client.ts @@ -3,6 +3,7 @@ import { container, SapphireClient } from "@sapphire/framework"; import { ActivityType, GatewayIntentBits, + Partials, PresenceUpdateStatus } from "discord.js"; import { PostHog } from "posthog-node"; @@ -11,6 +12,7 @@ declare module "@sapphire/framework" { interface Container { betterstack: Logtail; embed_authors: Map; + embed_messages: Map; posthog: PostHog; } } @@ -23,6 +25,7 @@ export class EmbedlyClient extends SapphireClient { GatewayIntentBits.GuildMessages, GatewayIntentBits.MessageContent ], + partials: [Partials.Message], presence: { activities: [ { @@ -45,6 +48,7 @@ export class EmbedlyClient extends SapphireClient { } ); container.embed_authors = new Map(); + container.embed_messages = new Map(); container.posthog = new PostHog(process.env.POSTHOG_API_KEY!, { host: process.env.POSTHOG_HOST }); diff --git a/apps/bot/src/commands/delete.ts b/apps/bot/src/commands/delete.ts index 9282200..0cb0452 100644 --- a/apps/bot/src/commands/delete.ts +++ b/apps/bot/src/commands/delete.ts @@ -10,6 +10,8 @@ import { Command } from "@sapphire/framework"; import { ApplicationCommandType, ApplicationIntegrationType, + type Guild, + type GuildMember, InteractionContextType, MessageFlags, PermissionFlagsBits @@ -108,10 +110,25 @@ export class DeleteCommand extends Command { } } - const guild = await interaction.guild!.fetch(); - const runner = await guild.members.fetch( - interaction.member.user.id - ); + let guild: Guild; + let runner: GuildMember; + try { + guild = await interaction.guild!.fetch(); + runner = await guild.members.fetch(interaction.member.user.id); + } catch { + this.container.betterstack.warn( + ...formatBetterStack(EMBEDLY_DELETE_FAILED_WARN, { + message_id: msg.id, + user_id: interaction.user.id, + reason: "failed_to_fetch_guild_or_member" + }) + ); + return await interaction.editReply({ + content: formatDiscord(EMBEDLY_DELETE_FAILED, { + message_id: msg.id + }) + }); + } const has_manage_permission = runner.permissions.has( PermissionFlagsBits.ManageMessages, @@ -138,6 +155,17 @@ export class DeleteCommand extends Command { await msg.delete(); this.container.embed_authors.delete(msg.id); + for (const [user_msg_id, bot_ids] of this.container + .embed_messages) { + const filtered = bot_ids.filter((id) => id !== msg.id); + if (filtered.length === bot_ids.length) continue; + if (filtered.length === 0) { + this.container.embed_messages.delete(user_msg_id); + } else { + this.container.embed_messages.set(user_msg_id, filtered); + } + break; + } this.container.betterstack.info( ...formatBetterStack(EMBEDLY_DELETE_SUCCESS_INFO, { message_id: msg.id, diff --git a/apps/bot/src/commands/embed.ts b/apps/bot/src/commands/embed.ts index 5c6ea29..5989159 100644 --- a/apps/bot/src/commands/embed.ts +++ b/apps/bot/src/commands/embed.ts @@ -146,7 +146,7 @@ export class EmbedCommand extends Command { if (error?.status === 400 || error?.status === 500) { const error_context = { ...log_ctx, - ...error.value.context! + ...("context" in error.value ? error.value.context : {}) }; this.container.betterstack.error( ...formatBetterStack(error.value, error_context) @@ -193,10 +193,17 @@ export class EmbedCommand extends Command { interaction: Command.ChatInputCommandInteraction ) { const url = interaction.options.getString("url", true); - const link_style = (await this.container.posthog.getFeatureFlag( - "embed-link-styling-test", - interaction.user.id - )) as EmbedFlags[EmbedFlagNames.LinkStyle] | undefined; + + let link_style: EmbedFlags[EmbedFlagNames.LinkStyle] | undefined; + try { + link_style = (await this.container.posthog.getFeatureFlag( + "embed-link-styling-test", + interaction.user.id + )) as EmbedFlags[EmbedFlagNames.LinkStyle] | undefined; + } catch { + link_style = undefined; + } + this.fetchEmbed(interaction, url, { [EmbedFlagNames.MediaOnly]: interaction.options.getBoolean("media_only") ?? false, diff --git a/apps/bot/src/listeners/messageCreate.ts b/apps/bot/src/listeners/messageCreate.ts index 308d395..bad6531 100644 --- a/apps/bot/src/listeners/messageCreate.ts +++ b/apps/bot/src/listeners/messageCreate.ts @@ -67,7 +67,7 @@ export class MessageListener extends Listener< if ("detail" in error.value) { const error_context: EmbedlyInteractionContext & EmbedlyPostContext = { - ...error.value.context, + ...("context" in error.value ? error.value.context : {}), message_id: message.id, user_id: message.author.id }; @@ -79,10 +79,17 @@ export class MessageListener extends Listener< } const embed = await Platforms[platform.type].createEmbed(data); - const link_style = (await this.container.posthog.getFeatureFlag( - "embed-link-styling-test", - message.author.id - )) as EmbedFlags[EmbedFlagNames.LinkStyle] | undefined; + + let link_style: EmbedFlags[EmbedFlagNames.LinkStyle] | undefined; + try { + link_style = (await this.container.posthog.getFeatureFlag( + "embed-link-styling-test", + message.author.id + )) as EmbedFlags[EmbedFlagNames.LinkStyle] | undefined; + } catch { + link_style = undefined; + } + const msg = { components: [ Embed.getDiscordEmbed(embed, { @@ -104,6 +111,10 @@ export class MessageListener extends Listener< bot_message.id, message.author.id ); + const existing = + this.container.embed_messages.get(message.id) ?? []; + existing.push(bot_message.id); + this.container.embed_messages.set(message.id, existing); this.container.betterstack.info( ...formatBetterStack(EMBEDLY_EMBED_CREATED_MESSAGE, { user_message_id: message.id, diff --git a/apps/bot/src/listeners/messageDelete.ts b/apps/bot/src/listeners/messageDelete.ts new file mode 100644 index 0000000..3369b71 --- /dev/null +++ b/apps/bot/src/listeners/messageDelete.ts @@ -0,0 +1,46 @@ +import { + EMBEDLY_AUTO_DELETE_INFO, + formatBetterStack +} from "@embedly/logging"; +import { Events, Listener } from "@sapphire/framework"; +import type { Message, PartialMessage } from "discord.js"; + +export class MessageDeleteListener extends Listener< + typeof Events.MessageDelete +> { + public constructor( + context: Listener.LoaderContext, + options: Listener.Options + ) { + super(context, { + ...options, + event: Events.MessageDelete + }); + } + + public async run(message: Message | PartialMessage) { + const bot_message_ids = this.container.embed_messages.get( + message.id + ); + if (!bot_message_ids) return; + + for (const bot_msg_id of bot_message_ids) { + try { + const bot_msg = + await message.channel.messages.fetch(bot_msg_id); + await bot_msg.delete(); + } catch {} + this.container.embed_authors.delete(bot_msg_id); + } + + this.container.embed_messages.delete(message.id); + + this.container.betterstack.info( + ...formatBetterStack(EMBEDLY_AUTO_DELETE_INFO, { + message_id: message.id, + user_id: message.author?.id, + original_author_id: message.author?.id + }) + ); + } +} diff --git a/packages/logging/src/main.ts b/packages/logging/src/main.ts index 219a74f..69a97c4 100644 --- a/packages/logging/src/main.ts +++ b/packages/logging/src/main.ts @@ -168,6 +168,14 @@ export const EMBEDLY_DELETE_SUCCESS_INFO: EmbedlyLogBase = detail: "User successfully deleted an embed." }; +export const EMBEDLY_AUTO_DELETE_INFO: EmbedlyLogBase = + { + type: "EMBEDLY_AUTO_DELETE_INFO", + title: "Embed auto-deleted.", + detail: + "Bot embed automatically deleted because the original message was deleted." + }; + export const EMBEDLY_NO_LINK_WARN: EmbedlyLogBase = { type: "EMBEDLY_NO_LINK_WARN", From 6b2990fabea0e036c4653a329ec2c317995c0537 Mon Sep 17 00:00:00 2001 From: ItsRauf <31735267+ItsRauf@users.noreply.github.com> Date: Tue, 10 Feb 2026 21:29:33 -0600 Subject: [PATCH 06/20] =?UTF-8?q?=F0=9F=92=A1=20chore(builder):=20remove?= =?UTF-8?q?=20unnecessary=20comments?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .changeset/hungry-wolves-drop.md | 10 ++++++++++ packages/builder/src/Embed.ts | 12 ------------ 2 files changed, 10 insertions(+), 12 deletions(-) create mode 100644 .changeset/hungry-wolves-drop.md diff --git a/.changeset/hungry-wolves-drop.md b/.changeset/hungry-wolves-drop.md new file mode 100644 index 0000000..04d0d28 --- /dev/null +++ b/.changeset/hungry-wolves-drop.md @@ -0,0 +1,10 @@ +--- +"@embedly/platforms": minor +"@embedly/builder": minor +"@embedly/logging": minor +"@embedly/config": minor +"@embedly/api": minor +"@embedly/bot": minor +--- + +Harden platform error handling, add messageDelete listener for automatic embed cleanup, and remove unnecessary comments diff --git a/packages/builder/src/Embed.ts b/packages/builder/src/Embed.ts index 3d31b50..d8ee033 100644 --- a/packages/builder/src/Embed.ts +++ b/packages/builder/src/Embed.ts @@ -137,18 +137,13 @@ export class Embed implements EmbedData { container.setSpoiler(true); } - // Add primary content section (reply content if exists, otherwise main content) Embed.addPrimaryContentSection(container, embed, source_only); - - // Add primary media (reply media if exists, otherwise main media) Embed.addPrimaryMedia(container, embed, source_only); - // Add secondary content (reply/quote) - skip if source_only if (!source_only) { Embed.addSecondaryContent(container, embed); } - // Add footer with stats and metadata Embed.addFooterSection(container, embed, link_style); return container.toJSON(); @@ -189,7 +184,6 @@ export class Embed implements EmbedData { prefix_emoji = !source_only && embed.quote ? statEmojis.quote : ""; } else { - // Show reply content first (only when not source_only) author_name = embed.replying_to.name; author_username = embed.replying_to.username; author_profile_url = embed.replying_to.profile_url; @@ -216,13 +210,10 @@ export class Embed implements EmbedData { ) { let media: APIMediaGalleryItem[] | undefined; if (source_only) { - // For source_only, use main embed's media media = embed.media; } else if (embed.replying_to) { - // For replies, show parent's media first (derek's media) media = embed.replying_to.media; } else { - // Otherwise use main embed's media media = embed.media; } @@ -241,16 +232,13 @@ export class Embed implements EmbedData { return; } - // Add separator container.addSeparatorComponents((builder) => builder.setDivider(true).setSpacing(SeparatorSpacingSize.Large) ); if (embed.replying_to) { - // Show original content after reply Embed.addOriginalContentAfterReply(container, embed); } else if (embed.quote) { - // Show quote content Embed.addQuoteContent(container, embed.quote); } } From 6fad47bdaea9ca3356245045da3c03184c76114b Mon Sep 17 00:00:00 2001 From: ItsRauf <31735267+ItsRauf@users.noreply.github.com> Date: Tue, 10 Feb 2026 23:55:39 -0600 Subject: [PATCH 07/20] =?UTF-8?q?=F0=9F=94=8A=20feat(bot):=20added=20otel?= =?UTF-8?q?=20for=20request=20tracing?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/bot/package.json | 12 +- apps/bot/src/client.ts | 3 + apps/bot/src/listeners/messageCreate.ts | 237 ++++++--- apps/bot/src/otel.ts | 34 ++ pnpm-lock.yaml | 629 +++++++++++++++++++++++- 5 files changed, 836 insertions(+), 79 deletions(-) create mode 100644 apps/bot/src/otel.ts diff --git a/apps/bot/package.json b/apps/bot/package.json index aec3d28..55ee77b 100644 --- a/apps/bot/package.json +++ b/apps/bot/package.json @@ -13,7 +13,7 @@ } }, "scripts": { - "dev": "tsx watch --env-file=.env src/main.ts", + "dev": "tsx watch --env-file=.env --import ./src/otel.ts src/main.ts", "lint": "biome ci ./src/*", "build": "pkgroll --sourcemap=inline && tsc" }, @@ -24,6 +24,16 @@ "@embedly/config": "workspace:*", "@embedly/logging": "workspace:*", "@embedly/platforms": "workspace:*", + "@opentelemetry/api": "^1.9.0", + "@opentelemetry/exporter-metrics-otlp-grpc": "^0.211.0", + "@opentelemetry/exporter-metrics-otlp-http": "^0.211.0", + "@opentelemetry/exporter-trace-otlp-grpc": "^0.211.0", + "@opentelemetry/exporter-trace-otlp-http": "^0.211.0", + "@opentelemetry/instrumentation-http": "^0.211.0", + "@opentelemetry/resources": "^2.5.0", + "@opentelemetry/sdk-metrics": "^2.5.0", + "@opentelemetry/sdk-node": "^0.211.0", + "@opentelemetry/semantic-conventions": "^1.39.0", "@types/node": "^25.0.3", "discord-api-types": "^0.38.37", "pkgroll": "^2.21.4", diff --git a/apps/bot/src/client.ts b/apps/bot/src/client.ts index 8a0a0c4..079eca4 100644 --- a/apps/bot/src/client.ts +++ b/apps/bot/src/client.ts @@ -1,4 +1,5 @@ import { Logtail } from "@logtail/node"; +import { type Tracer, trace } from "@opentelemetry/api"; import { container, SapphireClient } from "@sapphire/framework"; import { ActivityType, @@ -14,6 +15,7 @@ declare module "@sapphire/framework" { embed_authors: Map; embed_messages: Map; posthog: PostHog; + tracer: Tracer; } } @@ -52,6 +54,7 @@ export class EmbedlyClient extends SapphireClient { container.posthog = new PostHog(process.env.POSTHOG_API_KEY!, { host: process.env.POSTHOG_HOST }); + container.tracer = trace.getTracer("embedly-bot"); return super.login(token); } diff --git a/apps/bot/src/listeners/messageCreate.ts b/apps/bot/src/listeners/messageCreate.ts index bad6531..bac2b28 100644 --- a/apps/bot/src/listeners/messageCreate.ts +++ b/apps/bot/src/listeners/messageCreate.ts @@ -18,6 +18,7 @@ import Platforms, { isEscaped, isSpoiler } from "@embedly/platforms"; +import { SpanStatusCode } from "@opentelemetry/api"; import { Events, Listener } from "@sapphire/framework"; import { type Message, MessageFlags } from "discord.js"; @@ -42,89 +43,175 @@ export class MessageListener extends Listener< if (message.author.bot) return; if (message.author.id === this.container.client.id) return; if (!hasLink(message.content)) return; - const urls = message.content.match( new RegExp(GENERIC_LINK_REGEX, "g") ); if (!urls) return; - for (const [ind, url] of urls.entries()) { - if (isEscaped(url, message.content)) continue; - const platform = getPlatformFromURL(url); - if (!platform) continue; - const { data, error } = await app.api.scrape.post( - { - platform: platform.type, - url - }, - { - headers: { - authorization: `Bearer ${process.env.DISCORD_BOT_TOKEN}` - } - } - ); - 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 - }; - this.container.betterstack.error( - ...formatBetterStack(error.value, error_context) - ); - } - return; - } + this.container.tracer.startActiveSpan( + `message:${message.id}`, + async (root_span) => { + root_span.setAttributes({ + "discord.message_id": message.id, + "discord.guild_id": message.guildId ?? "dm", + "discord.channel_id": message.channelId, + "discord.user_id": message.author.id + }); - const embed = await Platforms[platform.type].createEmbed(data); + try { + for (const [ind, url] of urls.entries()) { + if (isEscaped(url, message.content)) continue; + const platform = + await this.container.tracer.startActiveSpan( + "detect_platform", + async (s) => { + const platform = getPlatformFromURL(url); + s.setAttribute( + "embedly.platform", + platform?.type ?? "unknown" + ); + s.setAttribute("embedly.url", url); + s.end(); + return platform; + } + ); + if (!platform) continue; - let link_style: EmbedFlags[EmbedFlagNames.LinkStyle] | undefined; - try { - link_style = (await this.container.posthog.getFeatureFlag( - "embed-link-styling-test", - message.author.id - )) as EmbedFlags[EmbedFlagNames.LinkStyle] | undefined; - } catch { - link_style = undefined; - } + 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 res = await app.api.scrape.post( + { + platform: platform.type, + url + }, + { + headers: { + authorization: `Bearer ${process.env.DISCORD_BOT_TOKEN}` + } + } + ); + 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 msg = { - components: [ - Embed.getDiscordEmbed(embed, { - [EmbedFlagNames.Spoiler]: isSpoiler(url, message.content), - [EmbedFlagNames.LinkStyle]: link_style ?? "control" - })! - ], - flags: MessageFlags.IsComponentsV2, - allowedMentions: { - parse: [], - repliedUser: false + 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 + }; + this.container.betterstack.error( + ...formatBetterStack(error.value, error_context) + ); + } + return; + } + + const embed = await this.container.tracer.startActiveSpan( + "create_embed", + async (s) => { + s.setAttribute("embedly.platform", platform.type); + const embed = + await Platforms[platform.type].createEmbed(data); + s.end(); + return embed; + } + ); + + let link_style: + | EmbedFlags[EmbedFlagNames.LinkStyle] + | undefined; + try { + link_style = (await this.container.posthog.getFeatureFlag( + "embed-link-styling-test", + message.author.id + )) as EmbedFlags[EmbedFlagNames.LinkStyle] | undefined; + } catch { + link_style = undefined; + } + + const msg = { + components: [ + Embed.getDiscordEmbed(embed, { + [EmbedFlagNames.Spoiler]: isSpoiler( + url, + message.content + ), + [EmbedFlagNames.LinkStyle]: link_style ?? "control" + })! + ], + flags: MessageFlags.IsComponentsV2, + allowedMentions: { + parse: [], + repliedUser: false + } + } as const; + const bot_message = + 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; + } + ); + this.container.embed_authors.set( + bot_message.id, + message.author.id + ); + const existing = + this.container.embed_messages.get(message.id) ?? []; + existing.push(bot_message.id); + this.container.embed_messages.set(message.id, existing); + this.container.betterstack.info( + ...formatBetterStack(EMBEDLY_EMBED_CREATED_MESSAGE, { + user_message_id: message.id, + bot_message_id: bot_message.id, + user_id: message.author.id, + platform: platform.type, + url + }) + ); + } + await message.edit({ flags: MessageFlags.SuppressEmbeds }); + root_span.setStatus({ code: SpanStatusCode.OK }); + } catch (error: any) { + root_span.setStatus({ + code: SpanStatusCode.ERROR, + message: error.message + }); + root_span.recordException(error); + } finally { + root_span.end(); } - } as const; - const bot_message = - ind > 0 && message.channel.isSendable() - ? await message.channel.send(msg) - : await message.reply(msg); - this.container.embed_authors.set( - bot_message.id, - message.author.id - ); - const existing = - this.container.embed_messages.get(message.id) ?? []; - existing.push(bot_message.id); - this.container.embed_messages.set(message.id, existing); - this.container.betterstack.info( - ...formatBetterStack(EMBEDLY_EMBED_CREATED_MESSAGE, { - user_message_id: message.id, - bot_message_id: bot_message.id, - user_id: message.author.id, - platform: platform.type, - url - }) - ); - } - await message.edit({ flags: MessageFlags.SuppressEmbeds }); + } + ); } } diff --git a/apps/bot/src/otel.ts b/apps/bot/src/otel.ts new file mode 100644 index 0000000..908a46b --- /dev/null +++ b/apps/bot/src/otel.ts @@ -0,0 +1,34 @@ +import { + DiagConsoleLogger, + DiagLogLevel, + diag +} from "@opentelemetry/api"; +import { OTLPMetricExporter } from "@opentelemetry/exporter-metrics-otlp-http"; +import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http"; +import { HttpInstrumentation } from "@opentelemetry/instrumentation-http"; +import { resourceFromAttributes } from "@opentelemetry/resources"; +import { PeriodicExportingMetricReader } from "@opentelemetry/sdk-metrics"; +import { NodeSDK } from "@opentelemetry/sdk-node"; +import { ATTR_SERVICE_NAME } from "@opentelemetry/semantic-conventions"; + +const endpoint = + process.env.OTEL_EXPORTER_OTLP_ENDPOINT ?? "http://lgtm:4317"; + +const sdk = new NodeSDK({ + resource: resourceFromAttributes({ + [ATTR_SERVICE_NAME]: "embedly-bot" + }), + traceExporter: new OTLPTraceExporter({ + url: `${endpoint}/v1/traces` + }), + metricReader: new PeriodicExportingMetricReader({ + exporter: new OTLPMetricExporter({ + url: `${endpoint}/v1/metrics` + }) + }), + instrumentations: [new HttpInstrumentation()] +}); + +diag.setLogger(new DiagConsoleLogger(), DiagLogLevel.ERROR); + +sdk.start(); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 919561e..0b899cb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -145,6 +145,36 @@ importers: '@embedly/platforms': specifier: workspace:* version: link:../../packages/platforms + '@opentelemetry/api': + specifier: ^1.9.0 + version: 1.9.0 + '@opentelemetry/exporter-metrics-otlp-grpc': + specifier: ^0.211.0 + version: 0.211.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-metrics-otlp-http': + specifier: ^0.211.0 + version: 0.211.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-trace-otlp-grpc': + specifier: ^0.211.0 + version: 0.211.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-trace-otlp-http': + specifier: ^0.211.0 + version: 0.211.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-http': + specifier: ^0.211.0 + version: 0.211.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': + specifier: ^2.5.0 + version: 2.5.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics': + specifier: ^2.5.0 + version: 2.5.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-node': + specifier: ^0.211.0 + version: 0.211.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': + specifier: ^1.39.0 + version: 1.39.0 '@types/node': specifier: ^25.0.3 version: 25.0.3 @@ -1012,6 +1042,15 @@ packages: '@gitmoji/parser-opts@1.4.0': resolution: {integrity: sha512-zzmx/vtpdB/ijjUm7u9OzHNCXWKpSbzVEgVzOzhilMgoTBlUDyInZFUtiCTV+Wf4oCP9nxGa/kQGQFfN+XLH1g==} + '@grpc/grpc-js@1.14.3': + resolution: {integrity: sha512-Iq8QQQ/7X3Sac15oB6p0FmUg/klxQvXLeileoqrTRGJYLV+/9tubbr9ipz0GKHjmXVsgFPo/+W+2cA8eNcR+XA==} + engines: {node: '>=12.10.0'} + + '@grpc/proto-loader@0.8.0': + resolution: {integrity: sha512-rc1hOQtjIWGxcxpb9aHAfLpIctjEnsDehj0DAiVfBlmT84uvR0uUtN2hEi/ecvWVjXUGf5qPF4qEgiLOx1YIMQ==} + engines: {node: '>=6'} + hasBin: true + '@img/colour@1.0.0': resolution: {integrity: sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==} engines: {node: '>=18'} @@ -1172,6 +1211,9 @@ packages: '@jridgewell/trace-mapping@0.3.9': resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} + '@js-sdsl/ordered-map@4.4.2': + resolution: {integrity: sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==} + '@logtail/core@0.5.6': resolution: {integrity: sha512-wbExeqFgH8mfEJ0N1X1KmBZVmh1oGyQLtHo329O9TvDqyL3tmyQnXbtEHwB0whfCKrkzHPZvWAamcv/Y8uPOZw==} @@ -1211,6 +1253,180 @@ packages: resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} engines: {node: '>= 8'} + '@opentelemetry/api-logs@0.211.0': + resolution: {integrity: sha512-swFdZq8MCdmdR22jTVGQDhwqDzcI4M10nhjXkLr1EsIzXgZBqm4ZlmmcWsg3TSNf+3mzgOiqveXmBLZuDi2Lgg==} + engines: {node: '>=8.0.0'} + + '@opentelemetry/api@1.9.0': + resolution: {integrity: sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==} + engines: {node: '>=8.0.0'} + + '@opentelemetry/configuration@0.211.0': + resolution: {integrity: sha512-PNsCkzsYQKyv8wiUIsH+loC4RYyblOaDnVASBtKS22hK55ToWs2UP6IsrcfSWWn54wWTvVe2gnfwz67Pvrxf2Q==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.9.0 + + '@opentelemetry/context-async-hooks@2.5.0': + resolution: {integrity: sha512-uOXpVX0ZjO7heSVjhheW2XEPrhQAWr2BScDPoZ9UDycl5iuHG+Usyc3AIfG6kZeC1GyLpMInpQ6X5+9n69yOFw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/core@2.5.0': + resolution: {integrity: sha512-ka4H8OM6+DlUhSAZpONu0cPBtPPTQKxbxVzC4CzVx5+K4JnroJVBtDzLAMx4/3CDTJXRvVFhpFjtl4SaiTNoyQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/exporter-logs-otlp-grpc@0.211.0': + resolution: {integrity: sha512-UhOoWENNqyaAMP/dL1YXLkXt6ZBtovkDDs1p4rxto9YwJX1+wMjwg+Obfyg2kwpcMoaiIFT3KQIcLNW8nNGNfQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/exporter-logs-otlp-http@0.211.0': + resolution: {integrity: sha512-c118Awf1kZirHkqxdcF+rF5qqWwNjJh+BB1CmQvN9AQHC/DUIldy6dIkJn3EKlQnQ3HmuNRKc/nHHt5IusN7mA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/exporter-logs-otlp-proto@0.211.0': + resolution: {integrity: sha512-kMvfKMtY5vJDXeLnwhrZMEwhZ2PN8sROXmzacFU/Fnl4Z79CMrOaL7OE+5X3SObRYlDUa7zVqaXp9ZetYCxfDQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/exporter-metrics-otlp-grpc@0.211.0': + resolution: {integrity: sha512-D/U3G8L4PzZp8ot5hX9wpgbTymgtLZCiwR7heMe4LsbGV4OdctS1nfyvaQHLT6CiGZ6FjKc1Vk9s6kbo9SWLXQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/exporter-metrics-otlp-http@0.211.0': + resolution: {integrity: sha512-lfHXElPAoDSPpPO59DJdN5FLUnwi1wxluLTWQDayqrSPfWRnluzxRhD+g7rF8wbj1qCz0sdqABl//ug1IZyWvA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/exporter-metrics-otlp-proto@0.211.0': + resolution: {integrity: sha512-61iNbffEpyZv/abHaz3BQM3zUtA2kVIDBM+0dS9RK68ML0QFLRGYa50xVMn2PYMToyfszEPEgFC3ypGae2z8FA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/exporter-prometheus@0.211.0': + resolution: {integrity: sha512-cD0WleEL3TPqJbvxwz5MVdVJ82H8jl8mvMad4bNU24cB5SH2mRW5aMLDTuV4614ll46R//R3RMmci26mc2L99g==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/exporter-trace-otlp-grpc@0.211.0': + resolution: {integrity: sha512-eFwx4Gvu6LaEiE1rOd4ypgAiWEdZu7Qzm2QNN2nJqPW1XDeAVH1eNwVcVQl+QK9HR/JCDZ78PZgD7xD/DBDqbw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/exporter-trace-otlp-http@0.211.0': + resolution: {integrity: sha512-F1Rv3JeMkgS//xdVjbQMrI3+26e5SXC7vXA6trx8SWEA0OUhw4JHB+qeHtH0fJn46eFItrYbL5m8j4qi9Sfaxw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/exporter-trace-otlp-proto@0.211.0': + resolution: {integrity: sha512-DkjXwbPiqpcPlycUojzG2RmR0/SIK8Gi9qWO9znNvSqgzrnAIE9x2n6yPfpZ+kWHZGafvsvA1lVXucTyyQa5Kg==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/exporter-zipkin@2.5.0': + resolution: {integrity: sha512-bk9VJgFgUAzkZzU8ZyXBSWiUGLOM3mZEgKJ1+jsZclhRnAoDNf+YBdq+G9R3cP0+TKjjWad+vVrY/bE/vRR9lA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.0.0 + + '@opentelemetry/instrumentation-http@0.211.0': + resolution: {integrity: sha512-n0IaQ6oVll9PP84SjbOCwDjaJasWRHi6BLsbMLiT6tNj7QbVOkuA5sk/EfZczwI0j5uTKl1awQPivO/ldVtsqA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation@0.211.0': + resolution: {integrity: sha512-h0nrZEC/zvI994nhg7EgQ8URIHt0uDTwN90r3qQUdZORS455bbx+YebnGeEuFghUT0HlJSrLF4iHw67f+odY+Q==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/otlp-exporter-base@0.211.0': + resolution: {integrity: sha512-bp1+63V8WPV+bRI9EQG6E9YID1LIHYSZVbp7f+44g9tRzCq+rtw/o4fpL5PC31adcUsFiz/oN0MdLISSrZDdrg==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/otlp-grpc-exporter-base@0.211.0': + resolution: {integrity: sha512-mR5X+N4SuphJeb7/K7y0JNMC8N1mB6gEtjyTLv+TSAhl0ZxNQzpSKP8S5Opk90fhAqVYD4R0SQSAirEBlH1KSA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/otlp-transformer@0.211.0': + resolution: {integrity: sha512-julhCJ9dXwkOg9svuuYqqjXLhVaUgyUvO2hWbTxwjvLXX2rG3VtAaB0SzxMnGTuoCZizBT7Xqqm2V7+ggrfCXA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/propagator-b3@2.5.0': + resolution: {integrity: sha512-g10m4KD73RjHrSvUge+sUxUl8m4VlgnGc6OKvo68a4uMfaLjdFU+AULfvMQE/APq38k92oGUxEzBsAZ8RN/YHg==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/propagator-jaeger@2.5.0': + resolution: {integrity: sha512-t70ErZCncAR/zz5AcGkL0TF25mJiK1FfDPEQCgreyAHZ+mRJ/bNUiCnImIBDlP3mSDXy6N09DbUEKq0ktW98Hg==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/resources@2.5.0': + resolution: {integrity: sha512-F8W52ApePshpoSrfsSk1H2yJn9aKjCrbpQF1M9Qii0GHzbfVeFUB+rc3X4aggyZD8x9Gu3Slua+s6krmq6Dt8g==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.3.0 <1.10.0' + + '@opentelemetry/sdk-logs@0.211.0': + resolution: {integrity: sha512-O5nPwzgg2JHzo59kpQTPUOTzFi0Nv5LxryG27QoXBciX3zWM3z83g+SNOHhiQVYRWFSxoWn1JM2TGD5iNjOwdA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.4.0 <1.10.0' + + '@opentelemetry/sdk-metrics@2.5.0': + resolution: {integrity: sha512-BeJLtU+f5Gf905cJX9vXFQorAr6TAfK3SPvTFqP+scfIpDQEJfRaGJWta7sJgP+m4dNtBf9y3yvBKVAZZtJQVA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.9.0 <1.10.0' + + '@opentelemetry/sdk-node@0.211.0': + resolution: {integrity: sha512-+s1eGjoqmPCMptNxcJJD4IxbWJKNLOQFNKhpwkzi2gLkEbCj6LzSHJNhPcLeBrBlBLtlSpibM+FuS7fjZ8SSFQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.3.0 <1.10.0' + + '@opentelemetry/sdk-trace-base@2.5.0': + resolution: {integrity: sha512-VzRf8LzotASEyNDUxTdaJ9IRJ1/h692WyArDBInf5puLCjxbICD6XkHgpuudis56EndyS7LYFmtTMny6UABNdQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.3.0 <1.10.0' + + '@opentelemetry/sdk-trace-node@2.5.0': + resolution: {integrity: sha512-O6N/ejzburFm2C84aKNrwJVPpt6HSTSq8T0ZUMq3xT2XmqT4cwxUItcL5UWGThYuq8RTcbH8u1sfj6dmRci0Ow==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/semantic-conventions@1.39.0': + resolution: {integrity: sha512-R5R9tb2AXs2IRLNKLBJDynhkfmx7mX0vi8NkhZb3gUkPWHn6HXk5J8iQ/dql0U3ApfWym4kXXmBDRGO+oeOfjg==} + engines: {node: '>=14'} + '@pnpm/config.env-replace@1.1.0': resolution: {integrity: sha512-htyl8TWnKL7K/ESFa1oW2UB5lVDxuF5DpM7tBi6Hu2LNL3mWkIzNLG6N4zoCUP1lCKNxWy/3iu8mS8MvToGd6w==} engines: {node: '>=12.22.0'} @@ -1235,6 +1451,36 @@ packages: '@posthog/core@1.9.1': resolution: {integrity: sha512-kRb1ch2dhQjsAapZmu6V66551IF2LnCbc1rnrQqnR7ArooVyJN9KOPXre16AJ3ObJz2eTfuP7x25BMyS2Y5Exw==} + '@protobufjs/aspromise@1.1.2': + resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==} + + '@protobufjs/base64@1.1.2': + resolution: {integrity: sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==} + + '@protobufjs/codegen@2.0.4': + resolution: {integrity: sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==} + + '@protobufjs/eventemitter@1.1.0': + resolution: {integrity: sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==} + + '@protobufjs/fetch@1.1.0': + resolution: {integrity: sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==} + + '@protobufjs/float@1.0.2': + resolution: {integrity: sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==} + + '@protobufjs/inquire@1.1.0': + resolution: {integrity: sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==} + + '@protobufjs/path@1.1.2': + resolution: {integrity: sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==} + + '@protobufjs/pool@1.1.0': + resolution: {integrity: sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==} + + '@protobufjs/utf8@1.1.0': + resolution: {integrity: sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==} + '@rollup/plugin-alias@6.0.0': resolution: {integrity: sha512-tPCzJOtS7uuVZd+xPhoy5W4vThe6KWXNmsFCNktaAh5RTqcLiSfT4huPQIXkgJ6YCOjJHvecOAzQxLFhPxKr+g==} engines: {node: '>=20.19.0'} @@ -1556,6 +1802,11 @@ packages: resolution: {integrity: sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==} hasBin: true + acorn-import-attributes@1.9.5: + resolution: {integrity: sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==} + peerDependencies: + acorn: ^8 + acorn-walk@8.3.2: resolution: {integrity: sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==} engines: {node: '>=0.4.0'} @@ -1565,6 +1816,11 @@ packages: engines: {node: '>=0.4.0'} hasBin: true + acorn@8.15.0: + resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==} + engines: {node: '>=0.4.0'} + hasBin: true + agent-base@7.1.4: resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} engines: {node: '>= 14'} @@ -1711,6 +1967,9 @@ packages: cjs-module-lexer@2.1.1: resolution: {integrity: sha512-+CmxIZ/L2vNcEfvNtLdU0ZQ6mbq3FZnwAP2PPTiKP+1QOoKwlKlPgb8UKV0Dds7QVaMnHm+FwSft2VB0s/SLjQ==} + cjs-module-lexer@2.2.0: + resolution: {integrity: sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ==} + cli-boxes@3.0.0: resolution: {integrity: sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g==} engines: {node: '>=10'} @@ -2095,6 +2354,9 @@ packages: resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==} engines: {node: '>=12.20.0'} + forwarded-parse@2.1.2: + resolution: {integrity: sha512-alTFZZQDKMporBH77856pXgzhEzaUVmLCDk+egLgIgHst3Tpndzz8MnKe+GzRJRfvVdn69HhpW7cmXzvtLvJAw==} + fs-extra@7.0.1: resolution: {integrity: sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==} engines: {node: '>=6 <7 || >=8'} @@ -2222,6 +2484,9 @@ packages: resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} engines: {node: '>=6'} + import-in-the-middle@2.0.6: + resolution: {integrity: sha512-3vZV3jX0XRFW3EJDTwzWoZa+RH1b8eTTx6YOCjglrLyPuepwoBti1k3L2dKwdCUrnVEfc5CuRuGstaC/uQJJaw==} + import-meta-resolve@4.2.0: resolution: {integrity: sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg==} @@ -2484,6 +2749,9 @@ packages: resolution: {integrity: sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw==} engines: {node: '>=18'} + long@5.3.2: + resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==} + lru-cache@7.18.3: resolution: {integrity: sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==} engines: {node: '>=12'} @@ -2545,6 +2813,9 @@ packages: minimist@1.2.8: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + module-details-from-path@1.0.4: + resolution: {integrity: sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w==} + mri@1.2.0: resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} engines: {node: '>=4'} @@ -2741,6 +3012,14 @@ packages: proto-list@1.2.4: resolution: {integrity: sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==} + protobufjs@7.5.4: + resolution: {integrity: sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==} + engines: {node: '>=12.0.0'} + + protobufjs@8.0.0: + resolution: {integrity: sha512-jx6+sE9h/UryaCZhsJWbJtTEy47yXoGNYI4z8ZaRncM0zBKeRqjO2JEcOUYwrYGb1WLhXM1FfMzW3annvFv0rw==} + engines: {node: '>=12.0.0'} + proxy-agent@6.5.0: resolution: {integrity: sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A==} engines: {node: '>= 14'} @@ -2786,6 +3065,10 @@ packages: resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} engines: {node: '>=0.10.0'} + require-in-the-middle@8.0.1: + resolution: {integrity: sha512-QT7FVMXfWOYFbeRBF6nu+I6tr2Tf3u0q8RIEjNob/heKY/nh7drD/k7eeMFmSQgnTtCzLDcCu/XEnpW2wk4xCQ==} + engines: {node: '>=9.3.0 || >=8.10.0 <9.0.0'} + resolve-from@4.0.0: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} @@ -3896,6 +4179,18 @@ snapshots: dependencies: '@gitmoji/gitmoji-regex': 1.0.0 + '@grpc/grpc-js@1.14.3': + dependencies: + '@grpc/proto-loader': 0.8.0 + '@js-sdsl/ordered-map': 4.4.2 + + '@grpc/proto-loader@0.8.0': + dependencies: + lodash.camelcase: 4.3.0 + long: 5.3.2 + protobufjs: 7.5.4 + yargs: 17.7.2 + '@img/colour@1.0.0': {} '@img/sharp-darwin-arm64@0.34.5': @@ -4010,6 +4305,8 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 + '@js-sdsl/ordered-map@4.4.2': {} + '@logtail/core@0.5.6': dependencies: '@logtail/tools': 0.5.6 @@ -4071,6 +4368,251 @@ snapshots: '@nodelib/fs.scandir': 2.1.5 fastq: 1.19.1 + '@opentelemetry/api-logs@0.211.0': + dependencies: + '@opentelemetry/api': 1.9.0 + + '@opentelemetry/api@1.9.0': {} + + '@opentelemetry/configuration@0.211.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.5.0(@opentelemetry/api@1.9.0) + yaml: 2.8.2 + + '@opentelemetry/context-async-hooks@2.5.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + + '@opentelemetry/core@2.5.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/semantic-conventions': 1.39.0 + + '@opentelemetry/exporter-logs-otlp-grpc@0.211.0(@opentelemetry/api@1.9.0)': + dependencies: + '@grpc/grpc-js': 1.14.3 + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.5.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.211.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-grpc-exporter-base': 0.211.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.211.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-logs': 0.211.0(@opentelemetry/api@1.9.0) + + '@opentelemetry/exporter-logs-otlp-http@0.211.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs': 0.211.0 + '@opentelemetry/core': 2.5.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.211.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.211.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-logs': 0.211.0(@opentelemetry/api@1.9.0) + + '@opentelemetry/exporter-logs-otlp-proto@0.211.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs': 0.211.0 + '@opentelemetry/core': 2.5.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.211.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.211.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.5.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-logs': 0.211.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 2.5.0(@opentelemetry/api@1.9.0) + + '@opentelemetry/exporter-metrics-otlp-grpc@0.211.0(@opentelemetry/api@1.9.0)': + dependencies: + '@grpc/grpc-js': 1.14.3 + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.5.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-metrics-otlp-http': 0.211.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.211.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-grpc-exporter-base': 0.211.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.211.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.5.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics': 2.5.0(@opentelemetry/api@1.9.0) + + '@opentelemetry/exporter-metrics-otlp-http@0.211.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.5.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.211.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.211.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.5.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics': 2.5.0(@opentelemetry/api@1.9.0) + + '@opentelemetry/exporter-metrics-otlp-proto@0.211.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.5.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-metrics-otlp-http': 0.211.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.211.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.211.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.5.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics': 2.5.0(@opentelemetry/api@1.9.0) + + '@opentelemetry/exporter-prometheus@0.211.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.5.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.5.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics': 2.5.0(@opentelemetry/api@1.9.0) + + '@opentelemetry/exporter-trace-otlp-grpc@0.211.0(@opentelemetry/api@1.9.0)': + dependencies: + '@grpc/grpc-js': 1.14.3 + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.5.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.211.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-grpc-exporter-base': 0.211.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.211.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.5.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 2.5.0(@opentelemetry/api@1.9.0) + + '@opentelemetry/exporter-trace-otlp-http@0.211.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.5.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.211.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.211.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.5.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 2.5.0(@opentelemetry/api@1.9.0) + + '@opentelemetry/exporter-trace-otlp-proto@0.211.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.5.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.211.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.211.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.5.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 2.5.0(@opentelemetry/api@1.9.0) + + '@opentelemetry/exporter-zipkin@2.5.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.5.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.5.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 2.5.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.39.0 + + '@opentelemetry/instrumentation-http@0.211.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.5.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.211.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.39.0 + forwarded-parse: 2.1.2 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation@0.211.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs': 0.211.0 + import-in-the-middle: 2.0.6 + require-in-the-middle: 8.0.1 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/otlp-exporter-base@0.211.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.5.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.211.0(@opentelemetry/api@1.9.0) + + '@opentelemetry/otlp-grpc-exporter-base@0.211.0(@opentelemetry/api@1.9.0)': + dependencies: + '@grpc/grpc-js': 1.14.3 + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.5.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.211.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.211.0(@opentelemetry/api@1.9.0) + + '@opentelemetry/otlp-transformer@0.211.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs': 0.211.0 + '@opentelemetry/core': 2.5.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.5.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-logs': 0.211.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics': 2.5.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 2.5.0(@opentelemetry/api@1.9.0) + protobufjs: 8.0.0 + + '@opentelemetry/propagator-b3@2.5.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.5.0(@opentelemetry/api@1.9.0) + + '@opentelemetry/propagator-jaeger@2.5.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.5.0(@opentelemetry/api@1.9.0) + + '@opentelemetry/resources@2.5.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.5.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.39.0 + + '@opentelemetry/sdk-logs@0.211.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs': 0.211.0 + '@opentelemetry/core': 2.5.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.5.0(@opentelemetry/api@1.9.0) + + '@opentelemetry/sdk-metrics@2.5.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.5.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.5.0(@opentelemetry/api@1.9.0) + + '@opentelemetry/sdk-node@0.211.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs': 0.211.0 + '@opentelemetry/configuration': 0.211.0(@opentelemetry/api@1.9.0) + '@opentelemetry/context-async-hooks': 2.5.0(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 2.5.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-logs-otlp-grpc': 0.211.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-logs-otlp-http': 0.211.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-logs-otlp-proto': 0.211.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-metrics-otlp-grpc': 0.211.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-metrics-otlp-http': 0.211.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-metrics-otlp-proto': 0.211.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-prometheus': 0.211.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-trace-otlp-grpc': 0.211.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-trace-otlp-http': 0.211.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-trace-otlp-proto': 0.211.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-zipkin': 2.5.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.211.0(@opentelemetry/api@1.9.0) + '@opentelemetry/propagator-b3': 2.5.0(@opentelemetry/api@1.9.0) + '@opentelemetry/propagator-jaeger': 2.5.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.5.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-logs': 0.211.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics': 2.5.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 2.5.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-node': 2.5.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.39.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/sdk-trace-base@2.5.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.5.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.5.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.39.0 + + '@opentelemetry/sdk-trace-node@2.5.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/context-async-hooks': 2.5.0(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 2.5.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 2.5.0(@opentelemetry/api@1.9.0) + + '@opentelemetry/semantic-conventions@1.39.0': {} + '@pnpm/config.env-replace@1.1.0': {} '@pnpm/network.ca-file@1.0.2': @@ -4099,6 +4641,29 @@ snapshots: dependencies: cross-spawn: 7.0.6 + '@protobufjs/aspromise@1.1.2': {} + + '@protobufjs/base64@1.1.2': {} + + '@protobufjs/codegen@2.0.4': {} + + '@protobufjs/eventemitter@1.1.0': {} + + '@protobufjs/fetch@1.1.0': + dependencies: + '@protobufjs/aspromise': 1.1.2 + '@protobufjs/inquire': 1.1.0 + + '@protobufjs/float@1.0.2': {} + + '@protobufjs/inquire@1.1.0': {} + + '@protobufjs/path@1.1.2': {} + + '@protobufjs/pool@1.1.0': {} + + '@protobufjs/utf8@1.1.0': {} + '@rollup/plugin-alias@6.0.0(rollup@4.53.5)': optionalDependencies: rollup: 4.53.5 @@ -4313,7 +4878,7 @@ snapshots: '@types/connect@3.4.38': dependencies: - '@types/node': 25.0.3 + '@types/node': 25.0.9 '@types/conventional-commits-parser@5.0.2': dependencies: @@ -4360,7 +4925,7 @@ snapshots: '@types/send@0.17.5': dependencies: '@types/mime': 1.3.5 - '@types/node': 25.0.3 + '@types/node': 25.0.9 '@types/serve-static@1.15.8': dependencies: @@ -4381,10 +4946,16 @@ snapshots: jsonparse: 1.3.1 through: 2.3.8 + acorn-import-attributes@1.9.5(acorn@8.15.0): + dependencies: + acorn: 8.15.0 + acorn-walk@8.3.2: {} acorn@8.14.0: {} + acorn@8.15.0: {} + agent-base@7.1.4: {} ajv-formats@3.0.1(ajv@8.17.1): @@ -4532,6 +5103,8 @@ snapshots: cjs-module-lexer@2.1.1: {} + cjs-module-lexer@2.2.0: {} + cli-boxes@3.0.0: {} cli-cursor@3.1.0: @@ -4992,6 +5565,8 @@ snapshots: dependencies: fetch-blob: 3.2.0 + forwarded-parse@2.1.2: {} + fs-extra@7.0.1: dependencies: graceful-fs: 4.2.11 @@ -5025,7 +5600,7 @@ snapshots: dependencies: basic-ftp: 5.0.5 data-uri-to-buffer: 6.0.2 - debug: 4.4.1 + debug: 4.4.3 transitivePeerDependencies: - supports-color @@ -5132,6 +5707,13 @@ snapshots: parent-module: 1.0.1 resolve-from: 4.0.0 + import-in-the-middle@2.0.6: + dependencies: + acorn: 8.15.0 + acorn-import-attributes: 1.9.5(acorn@8.15.0) + cjs-module-lexer: 2.2.0 + module-details-from-path: 1.0.4 + import-meta-resolve@4.2.0: {} inherits@2.0.4: {} @@ -5341,6 +5923,8 @@ snapshots: chalk: 5.6.2 is-unicode-supported: 1.3.0 + long@5.3.2: {} + lru-cache@7.18.3: {} magic-bytes.js@1.12.1: {} @@ -5396,6 +5980,8 @@ snapshots: minimist@1.2.8: {} + module-details-from-path@1.0.4: {} + mri@1.2.0: {} ms@2.1.3: {} @@ -5593,6 +6179,36 @@ snapshots: proto-list@1.2.4: {} + protobufjs@7.5.4: + dependencies: + '@protobufjs/aspromise': 1.1.2 + '@protobufjs/base64': 1.1.2 + '@protobufjs/codegen': 2.0.4 + '@protobufjs/eventemitter': 1.1.0 + '@protobufjs/fetch': 1.1.0 + '@protobufjs/float': 1.0.2 + '@protobufjs/inquire': 1.1.0 + '@protobufjs/path': 1.1.2 + '@protobufjs/pool': 1.1.0 + '@protobufjs/utf8': 1.1.0 + '@types/node': 25.0.9 + long: 5.3.2 + + protobufjs@8.0.0: + dependencies: + '@protobufjs/aspromise': 1.1.2 + '@protobufjs/base64': 1.1.2 + '@protobufjs/codegen': 2.0.4 + '@protobufjs/eventemitter': 1.1.0 + '@protobufjs/fetch': 1.1.0 + '@protobufjs/float': 1.0.2 + '@protobufjs/inquire': 1.1.0 + '@protobufjs/path': 1.1.2 + '@protobufjs/pool': 1.1.0 + '@protobufjs/utf8': 1.1.0 + '@types/node': 25.0.9 + long: 5.3.2 + proxy-agent@6.5.0: dependencies: agent-base: 7.1.4 @@ -5648,6 +6264,13 @@ snapshots: require-from-string@2.0.2: {} + require-in-the-middle@8.0.1: + dependencies: + debug: 4.4.3 + module-details-from-path: 1.0.4 + transitivePeerDependencies: + - supports-color + resolve-from@4.0.0: {} resolve-from@5.0.0: {} From c5f72df059a5a188db2621e800d63ce06831546f Mon Sep 17 00:00:00 2001 From: ItsRauf <31735267+ItsRauf@users.noreply.github.com> Date: Wed, 11 Feb 2026 00:12:41 -0600 Subject: [PATCH 08/20] =?UTF-8?q?=F0=9F=94=8A=20feat(bot):=20add=20tracing?= =?UTF-8?q?=20to=20embed=20command?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/bot/src/commands/embed.ts | 171 ++++++++++++++++++++++++++------- 1 file changed, 135 insertions(+), 36 deletions(-) diff --git a/apps/bot/src/commands/embed.ts b/apps/bot/src/commands/embed.ts index 5989159..c0c9c6b 100644 --- a/apps/bot/src/commands/embed.ts +++ b/apps/bot/src/commands/embed.ts @@ -20,6 +20,7 @@ import Platforms, { getPlatformFromURL, hasLink } from "@embedly/platforms"; +import { SpanStatusCode } from "@opentelemetry/api"; import { Command } from "@sapphire/framework"; import { ApplicationCommandType, @@ -118,7 +119,16 @@ export class EmbedCommand extends Command { }); } const url = GENERIC_LINK_REGEX.exec(content)![0]; - const platform = getPlatformFromURL(url); + const platform = await this.container.tracer.startActiveSpan( + "detect_platform", + async (s) => { + const platform = getPlatformFromURL(url); + s.setAttribute("embedly.platform", platform?.type ?? "unknown"); + s.setAttribute("embedly.url", url); + s.end(); + return platform; + } + ); if (!platform) { this.container.betterstack.warn( ...formatBetterStack(EMBEDLY_NO_VALID_LINK_WARN, log_ctx) @@ -131,15 +141,38 @@ export class EmbedCommand extends Command { await interaction.deferReply(); - const { data, error } = await app.api.scrape.post( - { - platform: platform.type, - url - }, - { - headers: { - authorization: `Bearer ${process.env.DISCORD_BOT_TOKEN}` + 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 res = await app.api.scrape.post( + { + platform: platform.type, + url + }, + { + headers: { + authorization: `Bearer ${process.env.DISCORD_BOT_TOKEN}` + } + } + ); + 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; } ); @@ -156,15 +189,33 @@ export class EmbedCommand extends Command { }); } - const embed = await Platforms[platform.type].createEmbed(data); - const bot_message = await interaction.editReply({ - components: [Embed.getDiscordEmbed(embed, flags)!], - flags: ["IsComponentsV2"], - allowedMentions: { - parse: [], - repliedUser: false + const embed = await this.container.tracer.startActiveSpan( + "create_embed", + async (s) => { + s.setAttribute("embedly.platform", platform.type); + const embed = await Platforms[platform.type].createEmbed(data); + s.end(); + return embed; } - }); + ); + + 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; + } + ); + this.container.embed_authors.set( bot_message.id, interaction.user.id @@ -186,7 +237,30 @@ export class EmbedCommand extends Command { ) { if (!interaction.isMessageContextMenuCommand()) return; const msg = interaction.targetMessage; - this.fetchEmbed(interaction, msg.content); + this.container.tracer.startActiveSpan( + `interaction:${interaction.id}`, + async (root_span) => { + root_span.setAttributes({ + "discord.interaction_id": interaction.id, + "discord.command": interaction.commandName, + "discord.guild_id": interaction.guildId ?? "dm", + "discord.user_id": interaction.user.id, + "discord.message_id": msg.id + }); + try { + await this.fetchEmbed(interaction, msg.content); + root_span.setStatus({ code: SpanStatusCode.OK }); + } catch (error: any) { + root_span.setStatus({ + code: SpanStatusCode.ERROR, + message: error.message + }); + root_span.recordException(error); + } finally { + root_span.end(); + } + } + ); } public override async chatInputRun( @@ -194,24 +268,49 @@ export class EmbedCommand extends Command { ) { const url = interaction.options.getString("url", true); - let link_style: EmbedFlags[EmbedFlagNames.LinkStyle] | undefined; - try { - link_style = (await this.container.posthog.getFeatureFlag( - "embed-link-styling-test", - interaction.user.id - )) as EmbedFlags[EmbedFlagNames.LinkStyle] | undefined; - } catch { - link_style = undefined; - } + this.container.tracer.startActiveSpan( + `interaction:${interaction.id}`, + async (root_span) => { + root_span.setAttributes({ + "discord.interaction_id": interaction.id, + "discord.command": interaction.commandName, + "discord.guild_id": interaction.guildId ?? "dm", + "discord.user_id": interaction.user.id + }); - this.fetchEmbed(interaction, url, { - [EmbedFlagNames.MediaOnly]: - interaction.options.getBoolean("media_only") ?? false, - [EmbedFlagNames.SourceOnly]: - interaction.options.getBoolean("source_only") ?? false, - [EmbedFlagNames.Spoiler]: - interaction.options.getBoolean("spoiler") ?? false, - [EmbedFlagNames.LinkStyle]: link_style ?? "control" - }); + let link_style: + | EmbedFlags[EmbedFlagNames.LinkStyle] + | undefined; + try { + link_style = (await this.container.posthog.getFeatureFlag( + "embed-link-styling-test", + interaction.user.id + )) as EmbedFlags[EmbedFlagNames.LinkStyle] | undefined; + } catch { + link_style = undefined; + } + + try { + await this.fetchEmbed(interaction, url, { + [EmbedFlagNames.MediaOnly]: + interaction.options.getBoolean("media_only") ?? false, + [EmbedFlagNames.SourceOnly]: + interaction.options.getBoolean("source_only") ?? false, + [EmbedFlagNames.Spoiler]: + interaction.options.getBoolean("spoiler") ?? false, + [EmbedFlagNames.LinkStyle]: link_style ?? "control" + }); + root_span.setStatus({ code: SpanStatusCode.OK }); + } catch (error: any) { + root_span.setStatus({ + code: SpanStatusCode.ERROR, + message: error.message + }); + root_span.recordException(error); + } finally { + root_span.end(); + } + } + ); } } From 10c23b70091dd146b7681b840566aa46d8da9d22 Mon Sep 17 00:00:00 2001 From: ItsRauf <31735267+ItsRauf@users.noreply.github.com> Date: Wed, 11 Feb 2026 01:21:24 -0600 Subject: [PATCH 09/20] =?UTF-8?q?=F0=9F=94=8A=20feat(api):=20add=20request?= =?UTF-8?q?=20tracing?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .changeset/wicked-cobras-like.md | 6 + apps/api/package.json | 2 + apps/api/src/main.ts | 247 +++++++++++++++++------- apps/api/worker-configuration.d.ts | 5 +- apps/bot/src/commands/embed.ts | 17 +- apps/bot/src/listeners/messageCreate.ts | 15 +- apps/bot/src/otel.ts | 16 +- pnpm-lock.yaml | 135 +++++++++++++ 8 files changed, 364 insertions(+), 79 deletions(-) create mode 100644 .changeset/wicked-cobras-like.md diff --git a/.changeset/wicked-cobras-like.md b/.changeset/wicked-cobras-like.md new file mode 100644 index 0000000..d2f3ff7 --- /dev/null +++ b/.changeset/wicked-cobras-like.md @@ -0,0 +1,6 @@ +--- +"@embedly/api": patch +"@embedly/bot": patch +--- + +added request tracing via otel diff --git a/apps/api/package.json b/apps/api/package.json index 7af4721..ced3301 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -34,6 +34,8 @@ "devDependencies": { "@biomejs/biome": "2.3.10", "@embedly/config": "workspace:*", + "@microlabs/otel-cf-workers": "1.0.0-rc.52", + "@opentelemetry/api": "^1.9.0", "@types/node": "^25.0.3", "discord-api-types": "^0.38.37", "pkgroll": "^2.21.4", diff --git a/apps/api/src/main.ts b/apps/api/src/main.ts index c7d21a5..18d27f6 100644 --- a/apps/api/src/main.ts +++ b/apps/api/src/main.ts @@ -13,6 +13,16 @@ import Platforms, { hasLink } from "@embedly/platforms"; import { Logtail } from "@logtail/edge"; +import { + instrument, + type ResolveConfigFn +} from "@microlabs/otel-cf-workers"; +import { + context, + propagation, + SpanStatusCode, + trace +} from "@opentelemetry/api"; import { Elysia, t } from "elysia"; const app = (env: Env, ctx: ExecutionContext) => @@ -54,79 +64,166 @@ const app = (env: Env, ctx: ExecutionContext) => }) .post( "/api/scrape", - async ({ body: { platform, url }, status, logger, set }) => { - const handler = Platforms[platform as keyof typeof Platforms]; - - let post_id: string; - try { - post_id = await handler.parsePostId(url); - } catch (error: any) { - logger.error("Failed to parse post ID", { - platform: handler.name, - url, - error: error.message || String(error) - }); - - return status(400, { - type: "EMBEDLY_INVALID_URL", - status: 400, - title: "Invalid URL", - detail: `Could not extract post ID from ${platform} URL` - }); - } + async ({ + body: { platform, url }, + status, + logger, + set, + headers + }) => { + const carrier: Record = {}; + Object.values(headers).forEach((value, key) => { + carrier[key] = value; + }); - const post_log_ctx: EmbedlyPostContext = { - platform: handler.name, - post_url: url, - post_id - }; - let post_data = await handler.getPostFromCache( - post_id, - env.STORAGE + const parent_ctx = propagation.extract( + context.active(), + carrier ); - if (!post_data) { - logger.debug( - ...formatBetterStack( - handler.log_messages.fetching, - post_log_ctx - ) - ); - - try { - post_data = await handler.fetchPost(post_id, env); - } catch (error: any) { - post_log_ctx.resp_status = error.code; - post_log_ctx.resp_message = error.message; - - const err = handler.log_messages.failed; - err.context = post_log_ctx; - - logger.error(...formatBetterStack(err, err.context)); - - return status(err.status!, err); - } + const tracer = trace.getTracer("embedly-api"); + return await context.with(parent_ctx, async () => { + return tracer.startActiveSpan("scrape", async (root_span) => { + const handler = + Platforms[platform as keyof typeof Platforms]; - if (!post_data) { - const err = handler.log_messages.failed; - return status(err.status!, err); - } + root_span.setAttributes({ + "embedly.platform": platform, + "embedly.url": url + }); - if (handler.name === EmbedlyPlatformType.Instagram) { - post_data!.url = url; - } + let post_id: string; + try { + post_id = await tracer.startActiveSpan( + "parse_post_id", + async (s) => { + const id = await handler.parsePostId(url); + s.setAttribute("embedly.post_id", id); + s.end(); + return id; + } + ); + } catch (error: any) { + root_span.setStatus({ + code: SpanStatusCode.ERROR, + message: error.message + }); + root_span.recordException(error); + root_span.end(); + return status(400, { + type: "EMBEDLY_INVALID_URL", + title: "Invalid URL.", + detail: `Failed to parse post ID from URL: ${url}` + }); + } - await handler.addPostToCache(post_id, post_data, env.STORAGE); - logger.debug( - ...formatBetterStack(EMBEDLY_CACHING_POST, post_log_ctx) - ); - } else { - logger.debug( - ...formatBetterStack(EMBEDLY_CACHED_POST, post_log_ctx) - ); - } + root_span.setAttribute("embedly.post_id", post_id); + + const post_log_ctx: EmbedlyPostContext = { + platform: handler.name, + post_url: url, + post_id + }; + let post_data = await tracer.startActiveSpan( + "cache_lookup", + async (s) => { + const res = await handler.getPostFromCache( + post_id, + env.STORAGE + ); + s.setAttribute("embedly.cache_hit", !!res); + s.end(); + return res; + } + ); + + if (!post_data) { + logger.debug( + ...formatBetterStack( + handler.log_messages.fetching, + post_log_ctx + ) + ); + + try { + post_data = await tracer.startActiveSpan( + "fetch_post", + async (s) => { + s.setAttribute("embedly.platform", handler.name); + try { + const data = await handler.fetchPost( + post_id, + env + ); + s.setStatus({ code: SpanStatusCode.OK }); + return data; + } catch (error: any) { + s.setStatus({ + code: SpanStatusCode.ERROR, + message: error.message + }); + s.recordException(error); + throw error; + } finally { + s.end(); + } + } + ); + } catch (error: any) { + post_log_ctx.resp_status = error.code; + post_log_ctx.resp_message = error.message; - set.headers["content-type"] = "application/json"; - return post_data as Record; + const err = handler.log_messages.failed; + err.context = post_log_ctx; + + logger.error(...formatBetterStack(err, err.context)); + + root_span.setStatus({ + code: SpanStatusCode.ERROR, + message: error.message + }); + root_span.end(); + return status(err.status!, err); + } + + if (!post_data) { + const err = handler.log_messages.failed; + + logger.error(...formatBetterStack(err, post_log_ctx)); + + root_span.setStatus({ + code: SpanStatusCode.ERROR, + message: err.detail + }); + root_span.end(); + return status(err.status!, err); + } + + if (handler.name === EmbedlyPlatformType.Instagram) { + post_data!.url = url; + } + + await tracer.startActiveSpan("cache_store", async (s) => { + handler.addPostToCache(post_id, post_data, env.STORAGE); + logger.debug( + ...formatBetterStack( + EMBEDLY_CACHING_POST, + post_log_ctx + ) + ); + s.end(); + }); + } else { + logger.debug( + ...formatBetterStack(EMBEDLY_CACHED_POST, post_log_ctx) + ); + } + + set.headers["content-type"] = "application/json"; + root_span.setStatus({ code: SpanStatusCode.OK }); + root_span.end(); + return post_data as Record; + }); + }); }, { body: t.Object({ @@ -196,10 +293,22 @@ const app = (env: Env, ctx: ExecutionContext) => } ); -export default { - fetch(req, env, ctx) { +const handler = { + fetch(req: Request, env: Env, ctx: ExecutionContext) { return app(env, ctx).fetch(req); } } satisfies ExportedHandler; +const config: ResolveConfigFn = (env: Env) => ({ + exporter: { + url: env.OTEL_ENDPOINT, + headers: {} + }, + service: { + name: "embedly-api" + } +}); + +export default instrument(handler, config); + export type App = ReturnType; diff --git a/apps/api/worker-configuration.d.ts b/apps/api/worker-configuration.d.ts index 130b2d2..76e6576 100644 --- a/apps/api/worker-configuration.d.ts +++ b/apps/api/worker-configuration.d.ts @@ -1,5 +1,5 @@ /* eslint-disable */ -// Generated by Wrangler by running `wrangler types` (hash: 37f49ce2d8d1275f19bca443420efd84) +// Generated by Wrangler by running `wrangler types` (hash: 44f998e2f4bea2fa3bbe96fe8c8510a9) // Runtime types generated with workerd@1.20260111.0 2025-06-23 nodejs_compat declare namespace Cloudflare { interface GlobalProps { @@ -14,6 +14,7 @@ declare namespace Cloudflare { EMBED_USER_AGENT: string; BETTERSTACK_SOURCE_TOKEN: string; BETTERSTACK_INGESTING_HOST: string; + OTEL_ENDPOINT: string; } } interface Env extends Cloudflare.Env {} @@ -21,7 +22,7 @@ type StringifyValues> = { [Binding in keyof EnvType]: EnvType[Binding] extends string ? EnvType[Binding] : string; }; declare namespace NodeJS { - interface ProcessEnv extends StringifyValues> {} + interface ProcessEnv extends StringifyValues> {} } // Begin runtime types diff --git a/apps/bot/src/commands/embed.ts b/apps/bot/src/commands/embed.ts index c0c9c6b..4e4b331 100644 --- a/apps/bot/src/commands/embed.ts +++ b/apps/bot/src/commands/embed.ts @@ -20,7 +20,11 @@ import Platforms, { getPlatformFromURL, hasLink } from "@embedly/platforms"; -import { SpanStatusCode } from "@opentelemetry/api"; +import { + context, + propagation, + SpanStatusCode +} from "@opentelemetry/api"; import { Command } from "@sapphire/framework"; import { ApplicationCommandType, @@ -146,6 +150,10 @@ export class EmbedCommand extends Command { 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, @@ -153,7 +161,8 @@ export class EmbedCommand extends Command { }, { headers: { - authorization: `Bearer ${process.env.DISCORD_BOT_TOKEN}` + authorization: `Bearer ${process.env.DISCORD_BOT_TOKEN}`, + ...otelHeaders } } ); @@ -238,7 +247,7 @@ export class EmbedCommand extends Command { if (!interaction.isMessageContextMenuCommand()) return; const msg = interaction.targetMessage; this.container.tracer.startActiveSpan( - `interaction:${interaction.id}`, + "context_menu", async (root_span) => { root_span.setAttributes({ "discord.interaction_id": interaction.id, @@ -269,7 +278,7 @@ export class EmbedCommand extends Command { const url = interaction.options.getString("url", true); this.container.tracer.startActiveSpan( - `interaction:${interaction.id}`, + "/embed", async (root_span) => { root_span.setAttributes({ "discord.interaction_id": interaction.id, diff --git a/apps/bot/src/listeners/messageCreate.ts b/apps/bot/src/listeners/messageCreate.ts index bac2b28..0a63a1f 100644 --- a/apps/bot/src/listeners/messageCreate.ts +++ b/apps/bot/src/listeners/messageCreate.ts @@ -18,7 +18,11 @@ import Platforms, { isEscaped, isSpoiler } from "@embedly/platforms"; -import { SpanStatusCode } from "@opentelemetry/api"; +import { + context, + propagation, + SpanStatusCode +} from "@opentelemetry/api"; import { Events, Listener } from "@sapphire/framework"; import { type Message, MessageFlags } from "discord.js"; @@ -49,7 +53,7 @@ export class MessageListener extends Listener< if (!urls) return; this.container.tracer.startActiveSpan( - `message:${message.id}`, + "message", async (root_span) => { root_span.setAttributes({ "discord.message_id": message.id, @@ -83,6 +87,10 @@ export class MessageListener extends Listener< 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, @@ -90,7 +98,8 @@ export class MessageListener extends Listener< }, { headers: { - authorization: `Bearer ${process.env.DISCORD_BOT_TOKEN}` + authorization: `Bearer ${process.env.DISCORD_BOT_TOKEN}`, + ...otelHeaders } } ); diff --git a/apps/bot/src/otel.ts b/apps/bot/src/otel.ts index 908a46b..f53060e 100644 --- a/apps/bot/src/otel.ts +++ b/apps/bot/src/otel.ts @@ -26,7 +26,21 @@ const sdk = new NodeSDK({ url: `${endpoint}/v1/metrics` }) }), - instrumentations: [new HttpInstrumentation()] + instrumentations: [ + new HttpInstrumentation({ + ignoreIncomingRequestHook: () => true, + ignoreOutgoingRequestHook: (request) => { + const host = + typeof request === "string" + ? request + : (request.hostname ?? request.host ?? ""); + + return ( + host.includes("discord.gg") || host.includes("discord.com") + ); + } + }) + ] }); diag.setLogger(new DiagConsoleLogger(), DiagLogLevel.ERROR); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0b899cb..a465bc4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -81,6 +81,12 @@ importers: '@embedly/config': specifier: workspace:* version: link:../../packages/config + '@microlabs/otel-cf-workers': + specifier: 1.0.0-rc.52 + version: 1.0.0-rc.52(@opentelemetry/api@1.9.0) + '@opentelemetry/api': + specifier: ^1.9.0 + version: 1.9.0 '@types/node': specifier: ^25.0.3 version: 25.0.3 @@ -1237,6 +1243,11 @@ packages: '@manypkg/get-packages@1.1.3': resolution: {integrity: sha512-fo+QhuU3qE/2TQMQmbVMqaQ6EWbMhi4ABWP+O4AM1NqPBuy0OrApV5LO6BrrgnhtAHS2NH6RrVk9OL181tTi8A==} + '@microlabs/otel-cf-workers@1.0.0-rc.52': + resolution: {integrity: sha512-7jf4sBNoaZCz2RSayefcvc/CZX0PDVA9aVk44bHpUebLrap+xjzDDw4T03klcYrBl/FdC7NamwvqjUMCtYdBvQ==} + peerDependencies: + '@opentelemetry/api': ~1.9.0 + '@msgpack/msgpack@2.8.0': resolution: {integrity: sha512-h9u4u/jiIRKbq25PM+zymTyW6bhTzELvOoUd+AvYriWOAKpLGnIamaET3pnHYoI5iYphAHBI4ayx0MehR+VVPQ==} engines: {node: '>= 10'} @@ -1253,6 +1264,10 @@ packages: resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} engines: {node: '>= 8'} + '@opentelemetry/api-logs@0.200.0': + resolution: {integrity: sha512-IKJBQxh91qJ+3ssRly5hYEJ8NDHu9oY/B1PXVSCWf7zytmYO9RNLB0Ox9XQ/fJ8m6gY6Q6NtBWlmXfaXt5Uc4Q==} + engines: {node: '>=8.0.0'} + '@opentelemetry/api-logs@0.211.0': resolution: {integrity: sha512-swFdZq8MCdmdR22jTVGQDhwqDzcI4M10nhjXkLr1EsIzXgZBqm4ZlmmcWsg3TSNf+3mzgOiqveXmBLZuDi2Lgg==} engines: {node: '>=8.0.0'} @@ -1273,6 +1288,12 @@ packages: peerDependencies: '@opentelemetry/api': '>=1.0.0 <1.10.0' + '@opentelemetry/core@2.0.0': + resolution: {integrity: sha512-SLX36allrcnVaPYG3R78F/UZZsBsvbc7lMCLx37LyH5MJ1KAAZ2E3mW9OAD3zGz0G8q/BtoS5VUrjzDydhD6LQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + '@opentelemetry/core@2.5.0': resolution: {integrity: sha512-ka4H8OM6+DlUhSAZpONu0cPBtPPTQKxbxVzC4CzVx5+K4JnroJVBtDzLAMx4/3CDTJXRvVFhpFjtl4SaiTNoyQ==} engines: {node: ^18.19.0 || >=20.6.0} @@ -1327,6 +1348,12 @@ packages: peerDependencies: '@opentelemetry/api': ^1.3.0 + '@opentelemetry/exporter-trace-otlp-http@0.200.0': + resolution: {integrity: sha512-Goi//m/7ZHeUedxTGVmEzH19NgqJY+Bzr6zXo1Rni1+hwqaksEyJ44gdlEMREu6dzX1DlAaH/qSykSVzdrdafA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + '@opentelemetry/exporter-trace-otlp-http@0.211.0': resolution: {integrity: sha512-F1Rv3JeMkgS//xdVjbQMrI3+26e5SXC7vXA6trx8SWEA0OUhw4JHB+qeHtH0fJn46eFItrYbL5m8j4qi9Sfaxw==} engines: {node: ^18.19.0 || >=20.6.0} @@ -1357,6 +1384,12 @@ packages: peerDependencies: '@opentelemetry/api': ^1.3.0 + '@opentelemetry/otlp-exporter-base@0.200.0': + resolution: {integrity: sha512-IxJgA3FD7q4V6gGq4bnmQM5nTIyMDkoGFGrBrrDjB6onEiq1pafma55V+bHvGYLWvcqbBbRfezr1GED88lacEQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + '@opentelemetry/otlp-exporter-base@0.211.0': resolution: {integrity: sha512-bp1+63V8WPV+bRI9EQG6E9YID1LIHYSZVbp7f+44g9tRzCq+rtw/o4fpL5PC31adcUsFiz/oN0MdLISSrZDdrg==} engines: {node: ^18.19.0 || >=20.6.0} @@ -1369,6 +1402,12 @@ packages: peerDependencies: '@opentelemetry/api': ^1.3.0 + '@opentelemetry/otlp-transformer@0.200.0': + resolution: {integrity: sha512-+9YDZbYybOnv7sWzebWOeK6gKyt2XE7iarSyBFkwwnP559pEevKOUD8NyDHhRjCSp13ybh9iVXlMfcj/DwF/yw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + '@opentelemetry/otlp-transformer@0.211.0': resolution: {integrity: sha512-julhCJ9dXwkOg9svuuYqqjXLhVaUgyUvO2hWbTxwjvLXX2rG3VtAaB0SzxMnGTuoCZizBT7Xqqm2V7+ggrfCXA==} engines: {node: ^18.19.0 || >=20.6.0} @@ -1387,18 +1426,36 @@ packages: peerDependencies: '@opentelemetry/api': '>=1.0.0 <1.10.0' + '@opentelemetry/resources@2.0.0': + resolution: {integrity: sha512-rnZr6dML2z4IARI4zPGQV4arDikF/9OXZQzrC01dLmn0CZxU5U5OLd/m1T7YkGRj5UitjeoCtg/zorlgMQcdTg==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.3.0 <1.10.0' + '@opentelemetry/resources@2.5.0': resolution: {integrity: sha512-F8W52ApePshpoSrfsSk1H2yJn9aKjCrbpQF1M9Qii0GHzbfVeFUB+rc3X4aggyZD8x9Gu3Slua+s6krmq6Dt8g==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': '>=1.3.0 <1.10.0' + '@opentelemetry/sdk-logs@0.200.0': + resolution: {integrity: sha512-VZG870063NLfObmQQNtCVcdXXLzI3vOjjrRENmU37HYiPFa0ZXpXVDsTD02Nh3AT3xYJzQaWKl2X2lQ2l7TWJA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.4.0 <1.10.0' + '@opentelemetry/sdk-logs@0.211.0': resolution: {integrity: sha512-O5nPwzgg2JHzo59kpQTPUOTzFi0Nv5LxryG27QoXBciX3zWM3z83g+SNOHhiQVYRWFSxoWn1JM2TGD5iNjOwdA==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': '>=1.4.0 <1.10.0' + '@opentelemetry/sdk-metrics@2.0.0': + resolution: {integrity: sha512-Bvy8QDjO05umd0+j+gDeWcTaVa1/R2lDj/eOvjzpm8VQj1K1vVZJuyjThpV5/lSHyYW2JaHF2IQ7Z8twJFAhjA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.9.0 <1.10.0' + '@opentelemetry/sdk-metrics@2.5.0': resolution: {integrity: sha512-BeJLtU+f5Gf905cJX9vXFQorAr6TAfK3SPvTFqP+scfIpDQEJfRaGJWta7sJgP+m4dNtBf9y3yvBKVAZZtJQVA==} engines: {node: ^18.19.0 || >=20.6.0} @@ -1411,6 +1468,12 @@ packages: peerDependencies: '@opentelemetry/api': '>=1.3.0 <1.10.0' + '@opentelemetry/sdk-trace-base@2.0.0': + resolution: {integrity: sha512-qQnYdX+ZCkonM7tA5iU4fSRsVxbFGml8jbxOgipRGMFHKaXKHQ30js03rTobYjKjIfnOsZSbHKWF0/0v0OQGfw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.3.0 <1.10.0' + '@opentelemetry/sdk-trace-base@2.5.0': resolution: {integrity: sha512-VzRf8LzotASEyNDUxTdaJ9IRJ1/h692WyArDBInf5puLCjxbICD6XkHgpuudis56EndyS7LYFmtTMny6UABNdQ==} engines: {node: ^18.19.0 || >=20.6.0} @@ -4354,6 +4417,17 @@ snapshots: globby: 11.1.0 read-yaml-file: 1.1.0 + '@microlabs/otel-cf-workers@1.0.0-rc.52(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.5.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-trace-otlp-http': 0.200.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.200.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.200.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.5.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 2.5.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.39.0 + '@msgpack/msgpack@2.8.0': {} '@nodelib/fs.scandir@2.1.5': @@ -4368,6 +4442,10 @@ snapshots: '@nodelib/fs.scandir': 2.1.5 fastq: 1.19.1 + '@opentelemetry/api-logs@0.200.0': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs@0.211.0': dependencies: '@opentelemetry/api': 1.9.0 @@ -4384,6 +4462,11 @@ snapshots: dependencies: '@opentelemetry/api': 1.9.0 + '@opentelemetry/core@2.0.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/semantic-conventions': 1.39.0 + '@opentelemetry/core@2.5.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 @@ -4468,6 +4551,15 @@ snapshots: '@opentelemetry/resources': 2.5.0(@opentelemetry/api@1.9.0) '@opentelemetry/sdk-trace-base': 2.5.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-trace-otlp-http@0.200.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.0.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.200.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.200.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.0.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 2.0.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-trace-otlp-http@0.211.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 @@ -4513,6 +4605,12 @@ snapshots: transitivePeerDependencies: - supports-color + '@opentelemetry/otlp-exporter-base@0.200.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.0.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.200.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base@0.211.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 @@ -4527,6 +4625,17 @@ snapshots: '@opentelemetry/otlp-exporter-base': 0.211.0(@opentelemetry/api@1.9.0) '@opentelemetry/otlp-transformer': 0.211.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer@0.200.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs': 0.200.0 + '@opentelemetry/core': 2.0.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.0.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-logs': 0.200.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics': 2.0.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 2.0.0(@opentelemetry/api@1.9.0) + protobufjs: 7.5.4 + '@opentelemetry/otlp-transformer@0.211.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 @@ -4548,12 +4657,25 @@ snapshots: '@opentelemetry/api': 1.9.0 '@opentelemetry/core': 2.5.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources@2.0.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.0.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.39.0 + '@opentelemetry/resources@2.5.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/core': 2.5.0(@opentelemetry/api@1.9.0) '@opentelemetry/semantic-conventions': 1.39.0 + '@opentelemetry/sdk-logs@0.200.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs': 0.200.0 + '@opentelemetry/core': 2.0.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.0.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-logs@0.211.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 @@ -4561,6 +4683,12 @@ snapshots: '@opentelemetry/core': 2.5.0(@opentelemetry/api@1.9.0) '@opentelemetry/resources': 2.5.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics@2.0.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.0.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.0.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics@2.5.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 @@ -4597,6 +4725,13 @@ snapshots: transitivePeerDependencies: - supports-color + '@opentelemetry/sdk-trace-base@2.0.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.0.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.0.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.39.0 + '@opentelemetry/sdk-trace-base@2.5.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 From ec5fd6a9f347526c84093971a5424a8c7acc2c61 Mon Sep 17 00:00:00 2001 From: ItsRauf <31735267+ItsRauf@users.noreply.github.com> Date: Wed, 11 Feb 2026 01:45:23 -0600 Subject: [PATCH 10/20] =?UTF-8?q?=F0=9F=90=9B=20fix(builder):=20limit=20de?= =?UTF-8?q?scriptions=20to=201500?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit discord has a limit of 4k and that was causing some posts to not embed --- .changeset/rare-moles-beg.md | 5 +++++ packages/builder/src/Embed.ts | 5 ++++- 2 files changed, 9 insertions(+), 1 deletion(-) create mode 100644 .changeset/rare-moles-beg.md diff --git a/.changeset/rare-moles-beg.md b/.changeset/rare-moles-beg.md new file mode 100644 index 0000000..b56c41d --- /dev/null +++ b/.changeset/rare-moles-beg.md @@ -0,0 +1,5 @@ +--- +"@embedly/builder": patch +--- + +limit descriptions to 1500 because discord has a global cap of 4k per message diff --git a/packages/builder/src/Embed.ts b/packages/builder/src/Embed.ts index d8ee033..33b2e7b 100644 --- a/packages/builder/src/Embed.ts +++ b/packages/builder/src/Embed.ts @@ -309,7 +309,10 @@ export class Embed implements EmbedData { if (description) { section.addTextDisplayComponents((builder) => - builder.setContent(description) + builder.setContent( + description.slice(0, 1500) + + (description.length > 1500 ? "..." : "") + ) ); } From 39ec0594fa8f3517dc9915de2747348431c34c65 Mon Sep 17 00:00:00 2001 From: ItsRauf <31735267+ItsRauf@users.noreply.github.com> Date: Fri, 13 Feb 2026 14:49:25 -0600 Subject: [PATCH 11/20] =?UTF-8?q?=F0=9F=94=8A=20feat(logging):=20replace?= =?UTF-8?q?=20formatBetterStack=20with=20OTEL-compatible=20formatLog?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/logging/src/main.ts | 29 ++++++++++++++++++++++++++--- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/packages/logging/src/main.ts b/packages/logging/src/main.ts index 69a97c4..de0d6ca 100644 --- a/packages/logging/src/main.ts +++ b/packages/logging/src/main.ts @@ -190,12 +190,35 @@ export const EMBEDLY_NO_VALID_LINK_WARN: EmbedlyLogBase( +export interface FormattedLog { + body: string; + attributes: Record; +} + +export function formatLog( log: T, ctx: T["context"] -) { +): FormattedLog { log.context = ctx; - return [log.detail, log] as const; + const contextAttrs: Record< + string, + string | number | boolean | undefined + > = {}; + if (ctx && typeof ctx === "object") { + for (const [key, value] of Object.entries(ctx)) { + if (value !== undefined && typeof value !== "object") { + contextAttrs[key] = value; + } + } + } + return { + body: log.detail, + attributes: { + "log.type": log.type, + "log.title": log.title, + ...contextAttrs + } + }; } export function formatDiscord< From 3146ad5b816c52f5892e7a99d0ec3b50101fafe0 Mon Sep 17 00:00:00 2001 From: ItsRauf <31735267+ItsRauf@users.noreply.github.com> Date: Fri, 13 Feb 2026 14:51:38 -0600 Subject: [PATCH 12/20] =?UTF-8?q?=F0=9F=94=8A=20feat(bot):=20add=20OTEL=20?= =?UTF-8?q?log=20exporter=20and=20graceful=20shutdown?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/bot/package.json | 3 +++ apps/bot/src/otel.ts | 17 +++++++++++++++++ pnpm-lock.yaml | 9 +++++++++ 3 files changed, 29 insertions(+) diff --git a/apps/bot/package.json b/apps/bot/package.json index 55ee77b..c557776 100644 --- a/apps/bot/package.json +++ b/apps/bot/package.json @@ -25,12 +25,15 @@ "@embedly/logging": "workspace:*", "@embedly/platforms": "workspace:*", "@opentelemetry/api": "^1.9.0", + "@opentelemetry/api-logs": "^0.211.0", + "@opentelemetry/exporter-logs-otlp-http": "^0.211.0", "@opentelemetry/exporter-metrics-otlp-grpc": "^0.211.0", "@opentelemetry/exporter-metrics-otlp-http": "^0.211.0", "@opentelemetry/exporter-trace-otlp-grpc": "^0.211.0", "@opentelemetry/exporter-trace-otlp-http": "^0.211.0", "@opentelemetry/instrumentation-http": "^0.211.0", "@opentelemetry/resources": "^2.5.0", + "@opentelemetry/sdk-logs": "^0.211.0", "@opentelemetry/sdk-metrics": "^2.5.0", "@opentelemetry/sdk-node": "^0.211.0", "@opentelemetry/semantic-conventions": "^1.39.0", diff --git a/apps/bot/src/otel.ts b/apps/bot/src/otel.ts index f53060e..6cea4cc 100644 --- a/apps/bot/src/otel.ts +++ b/apps/bot/src/otel.ts @@ -3,10 +3,12 @@ import { DiagLogLevel, diag } from "@opentelemetry/api"; +import { OTLPLogExporter } from "@opentelemetry/exporter-logs-otlp-http"; import { OTLPMetricExporter } from "@opentelemetry/exporter-metrics-otlp-http"; import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http"; import { HttpInstrumentation } from "@opentelemetry/instrumentation-http"; import { resourceFromAttributes } from "@opentelemetry/resources"; +import { BatchLogRecordProcessor } from "@opentelemetry/sdk-logs"; import { PeriodicExportingMetricReader } from "@opentelemetry/sdk-metrics"; import { NodeSDK } from "@opentelemetry/sdk-node"; import { ATTR_SERVICE_NAME } from "@opentelemetry/semantic-conventions"; @@ -21,6 +23,13 @@ const sdk = new NodeSDK({ traceExporter: new OTLPTraceExporter({ url: `${endpoint}/v1/traces` }), + logRecordProcessors: [ + new BatchLogRecordProcessor( + new OTLPLogExporter({ + url: `${endpoint}/v1/logs` + }) + ) + ], metricReader: new PeriodicExportingMetricReader({ exporter: new OTLPMetricExporter({ url: `${endpoint}/v1/metrics` @@ -46,3 +55,11 @@ const sdk = new NodeSDK({ diag.setLogger(new DiagConsoleLogger(), DiagLogLevel.ERROR); sdk.start(); + +const shutdown = async () => { + await sdk.shutdown(); + process.exit(0); +}; + +process.on("SIGTERM", shutdown); +process.on("SIGINT", shutdown); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a465bc4..a5c3395 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -154,6 +154,12 @@ importers: '@opentelemetry/api': specifier: ^1.9.0 version: 1.9.0 + '@opentelemetry/api-logs': + specifier: ^0.211.0 + version: 0.211.0 + '@opentelemetry/exporter-logs-otlp-http': + specifier: ^0.211.0 + version: 0.211.0(@opentelemetry/api@1.9.0) '@opentelemetry/exporter-metrics-otlp-grpc': specifier: ^0.211.0 version: 0.211.0(@opentelemetry/api@1.9.0) @@ -172,6 +178,9 @@ importers: '@opentelemetry/resources': specifier: ^2.5.0 version: 2.5.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-logs': + specifier: ^0.211.0 + version: 0.211.0(@opentelemetry/api@1.9.0) '@opentelemetry/sdk-metrics': specifier: ^2.5.0 version: 2.5.0(@opentelemetry/api@1.9.0) From 0d7d04fecdca57b783dc5935c6b88075d96369e4 Mon Sep 17 00:00:00 2001 From: ItsRauf <31735267+ItsRauf@users.noreply.github.com> Date: Fri, 13 Feb 2026 14:57:33 -0600 Subject: [PATCH 13/20] =?UTF-8?q?=F0=9F=94=8A=20feat(bot):=20replace=20Log?= =?UTF-8?q?tail=20with=20EmbedlyLogger?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit EmbedlyLogger implements Sapphire's logger interface for interop purposes as I overwrite the default logger --- apps/bot/package.json | 1 - apps/bot/src/client.ts | 108 +++++++++++++++++++++++++++++++++++++---- pnpm-lock.yaml | 3 -- 3 files changed, 98 insertions(+), 14 deletions(-) diff --git a/apps/bot/package.json b/apps/bot/package.json index c557776..54559c1 100644 --- a/apps/bot/package.json +++ b/apps/bot/package.json @@ -48,7 +48,6 @@ "@discordjs/rest": "^2.6.0", "@discordjs/ws": "^2.0.4", "@elysiajs/eden": "^1.4.5", - "@logtail/node": "^0.5.6", "@sapphire/discord.js-utilities": "^7.3.3", "@sapphire/framework": "^5.4.0", "discord.js": "~14.25.1", diff --git a/apps/bot/src/client.ts b/apps/bot/src/client.ts index 079eca4..ff756e5 100644 --- a/apps/bot/src/client.ts +++ b/apps/bot/src/client.ts @@ -1,6 +1,16 @@ -import { Logtail } from "@logtail/node"; +import type { FormattedLog } from "@embedly/logging"; import { type Tracer, trace } from "@opentelemetry/api"; -import { container, SapphireClient } from "@sapphire/framework"; +import { + logs, + type Logger as OtelLogger, + SeverityNumber +} from "@opentelemetry/api-logs"; +import { + container, + type ILogger, + LogLevel, + SapphireClient +} from "@sapphire/framework"; import { ActivityType, GatewayIntentBits, @@ -9,9 +19,89 @@ import { } from "discord.js"; import { PostHog } from "posthog-node"; +class EmbedlyLogger implements ILogger { + private otel: OtelLogger; + + constructor(name: string) { + this.otel = logs.getLogger(name); + } + + has(level: LogLevel): boolean { + return level >= LogLevel.Debug; + } + + trace(...values: readonly unknown[]) { + this.emit(SeverityNumber.TRACE, "TRACE", values); + } + + debug(...values: readonly unknown[]) { + this.emit(SeverityNumber.DEBUG, "DEBUG", values); + } + + info(...values: readonly unknown[]) { + this.emit(SeverityNumber.INFO, "INFO", values); + } + + warn(...values: readonly unknown[]) { + this.emit(SeverityNumber.WARN, "WARN", values); + } + + error(...values: readonly unknown[]) { + this.emit(SeverityNumber.ERROR, "ERROR", values); + } + + fatal(...values: readonly unknown[]) { + this.emit(SeverityNumber.FATAL, "FATAL", values); + } + + write(level: LogLevel, ...values: readonly unknown[]) { + const severityMap: Record = { + [LogLevel.Trace]: [SeverityNumber.TRACE, "TRACE"], + [LogLevel.Debug]: [SeverityNumber.DEBUG, "DEBUG"], + [LogLevel.Info]: [SeverityNumber.INFO, "INFO"], + [LogLevel.Warn]: [SeverityNumber.WARN, "WARN"], + [LogLevel.Error]: [SeverityNumber.ERROR, "ERROR"], + [LogLevel.Fatal]: [SeverityNumber.FATAL, "FATAL"], + [LogLevel.None]: [SeverityNumber.UNSPECIFIED, "UNSPECIFIED"] + }; + const [num, text] = severityMap[level] ?? [ + SeverityNumber.INFO, + "INFO" + ]; + this.emit(num, text, values); + } + + private emit( + severityNumber: SeverityNumber, + severityText: string, + values: readonly unknown[] + ) { + const first = values[0]; + if ( + first && + typeof first === "object" && + "body" in first && + "attributes" in first + ) { + const log = first as FormattedLog; + this.otel.emit({ + severityNumber, + severityText, + body: log.body, + attributes: log.attributes + }); + } else { + this.otel.emit({ + severityNumber, + severityText, + body: values.map(String).join(" ") + }); + } + } +} + declare module "@sapphire/framework" { interface Container { - betterstack: Logtail; embed_authors: Map; embed_messages: Map; posthog: PostHog; @@ -43,12 +133,7 @@ export class EmbedlyClient extends SapphireClient { } public override async login(token?: string) { - container.betterstack = new Logtail( - process.env.BETTERSTACK_SOURCE_TOKEN!, - { - endpoint: process.env.BETTERSTACK_INGESTING_HOST - } - ); + container.logger = new EmbedlyLogger("embedly-bot"); container.embed_authors = new Map(); container.embed_messages = new Map(); container.posthog = new PostHog(process.env.POSTHOG_API_KEY!, { @@ -59,7 +144,10 @@ export class EmbedlyClient extends SapphireClient { } public override async destroy() { - await container.betterstack?.flush(); + const provider = logs.getLoggerProvider() as { + forceFlush?: () => Promise; + }; + await provider.forceFlush?.(); await container.posthog.shutdown(); return super.destroy(); } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a5c3395..752b586 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -117,9 +117,6 @@ importers: '@elysiajs/eden': specifier: ^1.4.5 version: 1.4.5(elysia@1.4.19(@sinclair/typebox@0.34.38)(@types/bun@1.3.5)(exact-mirror@0.1.2(@sinclair/typebox@0.34.38))(file-type@21.0.0)(openapi-types@12.1.3)(typescript@5.9.3)) - '@logtail/node': - specifier: ^0.5.6 - version: 0.5.6 '@sapphire/discord.js-utilities': specifier: ^7.3.3 version: 7.3.3 From 03a14ebce5ae1510871e0fb57f30e5bcbd9bc64d Mon Sep 17 00:00:00 2001 From: ItsRauf <31735267+ItsRauf@users.noreply.github.com> Date: Fri, 13 Feb 2026 15:00:52 -0600 Subject: [PATCH 14/20] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor(bot):=20mig?= =?UTF-8?q?rate=20all=20logging=20call=20sites=20from=20Logtail=20to=20OTE?= =?UTF-8?q?L?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/bot/src/commands/delete.ts | 28 ++++++++++++------------- apps/bot/src/commands/embed.ts | 20 +++++++++--------- apps/bot/src/listeners/messageCreate.ts | 10 ++++----- apps/bot/src/listeners/messageDelete.ts | 9 +++----- 4 files changed, 32 insertions(+), 35 deletions(-) diff --git a/apps/bot/src/commands/delete.ts b/apps/bot/src/commands/delete.ts index 0cb0452..76c395c 100644 --- a/apps/bot/src/commands/delete.ts +++ b/apps/bot/src/commands/delete.ts @@ -3,8 +3,8 @@ import { EMBEDLY_DELETE_FAILED_WARN, EMBEDLY_DELETE_SUCCESS, EMBEDLY_DELETE_SUCCESS_INFO, - formatBetterStack, - formatDiscord + formatDiscord, + formatLog } from "@embedly/logging"; import { Command } from "@sapphire/framework"; import { @@ -54,8 +54,8 @@ export class DeleteCommand extends Command { if (!interaction.inGuild()) return; const msg = interaction.targetMessage; if (msg.author.id !== this.container.client.id) { - this.container.betterstack.warn( - ...formatBetterStack(EMBEDLY_DELETE_FAILED_WARN, { + this.container.logger.warn( + formatLog(EMBEDLY_DELETE_FAILED_WARN, { message_id: msg.id, user_id: interaction.user.id, reason: "not_bot_message" @@ -74,8 +74,8 @@ export class DeleteCommand extends Command { }); if (!msg.deletable) { - this.container.betterstack.warn( - ...formatBetterStack(EMBEDLY_DELETE_FAILED_WARN, { + this.container.logger.warn( + formatLog(EMBEDLY_DELETE_FAILED_WARN, { message_id: msg.id, user_id: interaction.user.id, reason: "not_deletable" @@ -95,8 +95,8 @@ export class DeleteCommand extends Command { const reference = await msg.fetchReference(); original_author_id = reference.author.id; } catch { - this.container.betterstack.warn( - ...formatBetterStack(EMBEDLY_DELETE_FAILED_WARN, { + this.container.logger.warn( + formatLog(EMBEDLY_DELETE_FAILED_WARN, { message_id: msg.id, user_id: interaction.user.id, reason: "no_author_mapping_and_no_reference" @@ -116,8 +116,8 @@ export class DeleteCommand extends Command { guild = await interaction.guild!.fetch(); runner = await guild.members.fetch(interaction.member.user.id); } catch { - this.container.betterstack.warn( - ...formatBetterStack(EMBEDLY_DELETE_FAILED_WARN, { + this.container.logger.warn( + formatLog(EMBEDLY_DELETE_FAILED_WARN, { message_id: msg.id, user_id: interaction.user.id, reason: "failed_to_fetch_guild_or_member" @@ -137,8 +137,8 @@ export class DeleteCommand extends Command { const is_original_poster = runner.id === original_author_id; if (!has_manage_permission && !is_original_poster) { - this.container.betterstack.warn( - ...formatBetterStack(EMBEDLY_DELETE_FAILED_WARN, { + this.container.logger.warn( + formatLog(EMBEDLY_DELETE_FAILED_WARN, { message_id: msg.id, user_id: interaction.user.id, original_author_id, @@ -166,8 +166,8 @@ export class DeleteCommand extends Command { } break; } - this.container.betterstack.info( - ...formatBetterStack(EMBEDLY_DELETE_SUCCESS_INFO, { + this.container.logger.info( + formatLog(EMBEDLY_DELETE_SUCCESS_INFO, { message_id: msg.id, user_id: interaction.user.id, original_author_id diff --git a/apps/bot/src/commands/embed.ts b/apps/bot/src/commands/embed.ts index 4e4b331..2b683f0 100644 --- a/apps/bot/src/commands/embed.ts +++ b/apps/bot/src/commands/embed.ts @@ -12,8 +12,8 @@ import { EMBEDLY_NO_VALID_LINK, EMBEDLY_NO_VALID_LINK_WARN, type EmbedlyInteractionContext, - formatBetterStack, - formatDiscord + formatDiscord, + formatLog } from "@embedly/logging"; import Platforms, { GENERIC_LINK_REGEX, @@ -114,8 +114,8 @@ export class EmbedCommand extends Command { user_id: interaction.user.id } satisfies EmbedlyInteractionContext; if (!hasLink(content)) { - this.container.betterstack.warn( - ...formatBetterStack(EMBEDLY_NO_LINK_WARN, log_ctx) + this.container.logger.warn( + formatLog(EMBEDLY_NO_LINK_WARN, log_ctx) ); return await interaction.reply({ content: formatDiscord(EMBEDLY_NO_LINK_IN_MESSAGE, log_ctx), @@ -134,8 +134,8 @@ export class EmbedCommand extends Command { } ); if (!platform) { - this.container.betterstack.warn( - ...formatBetterStack(EMBEDLY_NO_VALID_LINK_WARN, log_ctx) + this.container.logger.warn( + formatLog(EMBEDLY_NO_VALID_LINK_WARN, log_ctx) ); return await interaction.reply({ content: formatDiscord(EMBEDLY_NO_VALID_LINK, log_ctx), @@ -190,8 +190,8 @@ export class EmbedCommand extends Command { ...log_ctx, ...("context" in error.value ? error.value.context : {}) }; - this.container.betterstack.error( - ...formatBetterStack(error.value, error_context) + this.container.logger.error( + formatLog(error.value, error_context) ); return await interaction.editReply({ content: formatDiscord(error.value, error_context) @@ -229,8 +229,8 @@ export class EmbedCommand extends Command { bot_message.id, interaction.user.id ); - this.container.betterstack.info( - ...formatBetterStack(EMBEDLY_EMBED_CREATED_COMMAND, { + this.container.logger.info( + formatLog(EMBEDLY_EMBED_CREATED_COMMAND, { interaction_id: interaction.id, user_id: interaction.user.id, bot_message_id: bot_message.id, diff --git a/apps/bot/src/listeners/messageCreate.ts b/apps/bot/src/listeners/messageCreate.ts index 0a63a1f..7dfa9fb 100644 --- a/apps/bot/src/listeners/messageCreate.ts +++ b/apps/bot/src/listeners/messageCreate.ts @@ -9,7 +9,7 @@ import { EMBEDLY_EMBED_CREATED_MESSAGE, type EmbedlyInteractionContext, type EmbedlyPostContext, - formatBetterStack + formatLog } from "@embedly/logging"; import Platforms, { GENERIC_LINK_REGEX, @@ -132,8 +132,8 @@ export class MessageListener extends Listener< message_id: message.id, user_id: message.author.id }; - this.container.betterstack.error( - ...formatBetterStack(error.value, error_context) + this.container.logger.error( + formatLog(error.value, error_context) ); } return; @@ -199,8 +199,8 @@ export class MessageListener extends Listener< this.container.embed_messages.get(message.id) ?? []; existing.push(bot_message.id); this.container.embed_messages.set(message.id, existing); - this.container.betterstack.info( - ...formatBetterStack(EMBEDLY_EMBED_CREATED_MESSAGE, { + this.container.logger.info( + formatLog(EMBEDLY_EMBED_CREATED_MESSAGE, { user_message_id: message.id, bot_message_id: bot_message.id, user_id: message.author.id, diff --git a/apps/bot/src/listeners/messageDelete.ts b/apps/bot/src/listeners/messageDelete.ts index 3369b71..27be50e 100644 --- a/apps/bot/src/listeners/messageDelete.ts +++ b/apps/bot/src/listeners/messageDelete.ts @@ -1,7 +1,4 @@ -import { - EMBEDLY_AUTO_DELETE_INFO, - formatBetterStack -} from "@embedly/logging"; +import { EMBEDLY_AUTO_DELETE_INFO, formatLog } from "@embedly/logging"; import { Events, Listener } from "@sapphire/framework"; import type { Message, PartialMessage } from "discord.js"; @@ -35,8 +32,8 @@ export class MessageDeleteListener extends Listener< this.container.embed_messages.delete(message.id); - this.container.betterstack.info( - ...formatBetterStack(EMBEDLY_AUTO_DELETE_INFO, { + this.container.logger.info( + formatLog(EMBEDLY_AUTO_DELETE_INFO, { message_id: message.id, user_id: message.author?.id, original_author_id: message.author?.id From 34c6099ab83442eac51e13e2703d4ff38fff5cbf Mon Sep 17 00:00:00 2001 From: ItsRauf <31735267+ItsRauf@users.noreply.github.com> Date: Fri, 13 Feb 2026 15:03:40 -0600 Subject: [PATCH 15/20] =?UTF-8?q?=E2=9C=A8=20feat(api):=20replace=20Logtai?= =?UTF-8?q?l=20with=20dual=20console+Loki=20logger=20via=20OTLP=20HTTP?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/api/package.json | 2 - apps/api/src/main.ts | 126 ++++++++++++++++++++++++++++++++++++------ pnpm-lock.yaml | 109 +----------------------------------- 3 files changed, 111 insertions(+), 126 deletions(-) diff --git a/apps/api/package.json b/apps/api/package.json index ced3301..ece0a8a 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -25,8 +25,6 @@ "@embedly/builder": "workspace:*", "@embedly/logging": "workspace:*", "@embedly/platforms": "workspace:*", - "@logtail/edge": "^0.5.7", - "@logtail/node": "^0.5.6", "discord-verify": "^1.2.0", "elysia": "^1.4.19", "wrangler": "^4.59.1" diff --git a/apps/api/src/main.ts b/apps/api/src/main.ts index 18d27f6..01dde83 100644 --- a/apps/api/src/main.ts +++ b/apps/api/src/main.ts @@ -4,7 +4,8 @@ import { EMBEDLY_NO_LINK_IN_MESSAGE, EMBEDLY_NO_VALID_LINK, type EmbedlyPostContext, - formatBetterStack + type FormattedLog, + formatLog } from "@embedly/logging"; import Platforms, { EmbedlyPlatformType, @@ -12,7 +13,6 @@ import Platforms, { getPlatformFromURL, hasLink } from "@embedly/platforms"; -import { Logtail } from "@logtail/edge"; import { instrument, type ResolveConfigFn @@ -33,13 +33,111 @@ const app = (env: Env, ctx: ExecutionContext) => }) .decorate({ env, ctx }) .derive(({ ctx, env }) => { - const logtail = new Logtail(env.BETTERSTACK_SOURCE_TOKEN, { - endpoint: env.BETTERSTACK_INGESTING_HOST - }); + const pending: { + severityText: string; + severityNumber: number; + log: FormattedLog; + timestamp: number; + }[] = []; + + const emit = ( + severityText: string, + severityNumber: number, + log: FormattedLog + ) => { + const method = severityText.toLowerCase() as + | "debug" + | "info" + | "warn" + | "error"; + console[method]?.(log.body, log.attributes); + pending.push({ + severityText, + severityNumber, + log, + timestamp: Date.now() + }); + }; + + const flush = () => { + if (pending.length === 0) return; + const body = { + resourceLogs: [ + { + resource: { + attributes: [ + { + key: "service.name", + value: { stringValue: "embedly-api" } + } + ] + }, + scopeLogs: [ + { + scope: { name: "embedly-api" }, + logRecords: pending.map( + ({ + severityText, + severityNumber, + log, + timestamp + }) => ({ + timeUnixNano: String(timestamp * 1_000_000), + severityNumber, + severityText, + body: { stringValue: log.body }, + attributes: Object.entries(log.attributes) + .filter(([, v]) => v !== undefined) + .map(([key, value]) => ({ + key, + value: + typeof value === "number" + ? { intValue: value } + : typeof value === "boolean" + ? { boolValue: value } + : { stringValue: String(value) } + })) + }) + ) + } + ] + } + ] + }; + + ctx.waitUntil( + fetch( + `${env.OTEL_ENDPOINT.replace("/v1/traces", "")}/v1/logs`, + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(body) + } + ).catch(() => {}) + ); + }; + return { - logger: logtail.withExecutionContext(ctx) + logger: { + debug(log: FormattedLog) { + emit("DEBUG", 5, log); + }, + info(log: FormattedLog) { + emit("INFO", 9, log); + }, + warn(log: FormattedLog) { + emit("WARN", 13, log); + }, + error(log: FormattedLog) { + emit("ERROR", 17, log); + }, + flush + } }; }) + .onAfterHandle(({ logger }) => { + logger.flush(); + }) .guard({ headers: t.Object( { @@ -138,10 +236,7 @@ const app = (env: Env, ctx: ExecutionContext) => if (!post_data) { logger.debug( - ...formatBetterStack( - handler.log_messages.fetching, - post_log_ctx - ) + formatLog(handler.log_messages.fetching, post_log_ctx) ); try { @@ -175,7 +270,7 @@ const app = (env: Env, ctx: ExecutionContext) => const err = handler.log_messages.failed; err.context = post_log_ctx; - logger.error(...formatBetterStack(err, err.context)); + logger.error(formatLog(err, err.context)); root_span.setStatus({ code: SpanStatusCode.ERROR, @@ -188,7 +283,7 @@ const app = (env: Env, ctx: ExecutionContext) => if (!post_data) { const err = handler.log_messages.failed; - logger.error(...formatBetterStack(err, post_log_ctx)); + logger.error(formatLog(err, post_log_ctx)); root_span.setStatus({ code: SpanStatusCode.ERROR, @@ -205,16 +300,13 @@ const app = (env: Env, ctx: ExecutionContext) => await tracer.startActiveSpan("cache_store", async (s) => { handler.addPostToCache(post_id, post_data, env.STORAGE); logger.debug( - ...formatBetterStack( - EMBEDLY_CACHING_POST, - post_log_ctx - ) + formatLog(EMBEDLY_CACHING_POST, post_log_ctx) ); s.end(); }); } else { logger.debug( - ...formatBetterStack(EMBEDLY_CACHED_POST, post_log_ctx) + formatLog(EMBEDLY_CACHED_POST, post_log_ctx) ); } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 752b586..2b58ac2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -59,12 +59,6 @@ importers: '@embedly/platforms': specifier: workspace:* version: link:../../packages/platforms - '@logtail/edge': - specifier: ^0.5.7 - version: 0.5.7(@cloudflare/workers-types@4.20251220.0) - '@logtail/node': - specifier: ^0.5.6 - version: 0.5.6 discord-verify: specifier: ^1.2.0 version: 1.2.0 @@ -73,7 +67,7 @@ importers: version: 1.4.19(@sinclair/typebox@0.34.38)(@types/bun@1.3.5)(exact-mirror@0.1.2(@sinclair/typebox@0.34.38))(file-type@21.0.0)(openapi-types@12.1.3)(typescript@5.9.3) wrangler: specifier: ^4.59.1 - version: 4.59.1(@cloudflare/workers-types@4.20251220.0) + version: 4.59.1 devDependencies: '@biomejs/biome': specifier: 2.3.10 @@ -1226,23 +1220,6 @@ packages: '@js-sdsl/ordered-map@4.4.2': resolution: {integrity: sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==} - '@logtail/core@0.5.6': - resolution: {integrity: sha512-wbExeqFgH8mfEJ0N1X1KmBZVmh1oGyQLtHo329O9TvDqyL3tmyQnXbtEHwB0whfCKrkzHPZvWAamcv/Y8uPOZw==} - - '@logtail/edge@0.5.7': - resolution: {integrity: sha512-joxu8ygUOc6VuQyhSK6dRhqi5VS7lNIFUfy/m14mcmFJZ4hPplAI8qwoQ23UPkDmClyytkgLeNIH/FTatrrV6g==} - peerDependencies: - '@cloudflare/workers-types': ^4.20230904.0 - - '@logtail/node@0.5.6': - resolution: {integrity: sha512-j+Q/LXVrZa6p+7qfNtiT5ANqmrusrSpBXU5Cxe6Wm+tB48NLaNdOiFz1EgqgTW50WzwlTHYl7ZhfPGsYgmyMOA==} - - '@logtail/tools@0.5.6': - resolution: {integrity: sha512-l6VIzOUGZs6eLIWjZD88hNyE61xjRN0xypvFvY7CGl9nH+MlBSoFobA8XPTB+JpU1Zn3UhovOJrRM2SAoVyq4Q==} - - '@logtail/types@0.5.6': - resolution: {integrity: sha512-a7tkl2rBE24SoV1dG3Jme6sfSRtQjSonlVym0PvoBHFbretqm0FB1SQuba27zOC9qH0kcYtyJGkoFi4vpJE/tQ==} - '@manypkg/find-root@1.1.0': resolution: {integrity: sha512-mki5uBvhHzO8kYYix/WRy2WX8S3B5wdVSc9D6KcU5lQNglP2yt58/VfLuAK49glRXChosY8ap2oJ1qgma3GUVA==} @@ -1254,10 +1231,6 @@ packages: peerDependencies: '@opentelemetry/api': ~1.9.0 - '@msgpack/msgpack@2.8.0': - resolution: {integrity: sha512-h9u4u/jiIRKbq25PM+zymTyW6bhTzELvOoUd+AvYriWOAKpLGnIamaET3pnHYoI5iYphAHBI4ayx0MehR+VVPQ==} - engines: {node: '>= 10'} - '@nodelib/fs.scandir@2.1.5': resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -1857,9 +1830,6 @@ packages: '@types/serve-static@1.15.8': resolution: {integrity: sha512-roei0UY3LhpOJvjbIP6ZZFngyLKl5dskOtDhxY5THRSpO+ZI+nzJ+m5yUMzGrp89YRa7lvknKkMYjqQFGwA7Sg==} - '@types/stack-trace@0.0.33': - resolution: {integrity: sha512-O7in6531Bbvlb2KEsJ0dq0CHZvc3iWSR5ZYMtvGgnHA56VgriAN/AU2LorfmcvAl2xc9N5fbCTRyMRRl8nd74g==} - '@types/ws@8.18.1': resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==} @@ -1960,9 +1930,6 @@ packages: atomically@2.0.3: resolution: {integrity: sha512-kU6FmrwZ3Lx7/7y3hPS5QnbJfaohcIul5fGqf7ok+4KklIEk9tJ0C2IQPdacSbVUWv6zVHXEBWoWd6NrVMT7Cw==} - balanced-match@1.0.2: - resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} - base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} @@ -1987,9 +1954,6 @@ packages: resolution: {integrity: sha512-F3PH5k5juxom4xktynS7MoFY+NUWH5LC4CnH11YB8NPew+HLpmBLCybSAEyb2F+4pRXhuhWqFesoQd6DAyc2hw==} engines: {node: '>=18'} - brace-expansion@2.0.2: - resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} - braces@3.0.3: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} @@ -2875,10 +2839,6 @@ packages: engines: {node: '>=18.0.0'} hasBin: true - minimatch@9.0.5: - resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} - engines: {node: '>=16 || 14 >=14.17'} - minimist@1.2.8: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} @@ -3204,10 +3164,6 @@ packages: engines: {node: '>=10'} hasBin: true - serialize-error@8.1.0: - resolution: {integrity: sha512-3NnuWfM6vBYoy5gZFvHiYsVbafvI9vZv/+jlIigFn4oP4zjNPK3LhcY0xSCgeb1a5L8jO71Mit9LlNoi2UfDDQ==} - engines: {node: '>=10'} - sharp@0.34.5: resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} @@ -3257,9 +3213,6 @@ packages: sprintf-js@1.0.3: resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} - stack-trace@0.0.10: - resolution: {integrity: sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==} - stdin-discarder@0.2.2: resolution: {integrity: sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==} engines: {node: '>=18'} @@ -3393,10 +3346,6 @@ packages: resolution: {integrity: sha512-1dUGwi6cSSVZts1BwJa/Gh7w5dPNNGsNWZEAuRKxXWME44hTKWpQZrgiPnqMc5jJJOovzPK5N6tL+PHYRYL5Wg==} hasBin: true - type-fest@0.20.2: - resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} - engines: {node: '>=10'} - type-fest@0.21.3: resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==} engines: {node: '>=10'} @@ -4376,37 +4325,6 @@ snapshots: '@js-sdsl/ordered-map@4.4.2': {} - '@logtail/core@0.5.6': - dependencies: - '@logtail/tools': 0.5.6 - '@logtail/types': 0.5.6 - serialize-error: 8.1.0 - - '@logtail/edge@0.5.7(@cloudflare/workers-types@4.20251220.0)': - dependencies: - '@cloudflare/workers-types': 4.20251220.0 - '@logtail/core': 0.5.6 - '@logtail/types': 0.5.6 - '@msgpack/msgpack': 2.8.0 - '@types/stack-trace': 0.0.33 - minimatch: 9.0.5 - stack-trace: 0.0.10 - - '@logtail/node@0.5.6': - dependencies: - '@logtail/core': 0.5.6 - '@logtail/types': 0.5.6 - '@msgpack/msgpack': 2.8.0 - '@types/stack-trace': 0.0.33 - minimatch: 9.0.5 - stack-trace: 0.0.10 - - '@logtail/tools@0.5.6': - dependencies: - '@logtail/types': 0.5.6 - - '@logtail/types@0.5.6': {} - '@manypkg/find-root@1.1.0': dependencies: '@babel/runtime': 7.28.4 @@ -4434,8 +4352,6 @@ snapshots: '@opentelemetry/sdk-trace-base': 2.5.0(@opentelemetry/api@1.9.0) '@opentelemetry/semantic-conventions': 1.39.0 - '@msgpack/msgpack@2.8.0': {} - '@nodelib/fs.scandir@2.1.5': dependencies: '@nodelib/fs.stat': 2.0.5 @@ -5074,8 +4990,6 @@ snapshots: '@types/node': 25.0.3 '@types/send': 0.17.5 - '@types/stack-trace@0.0.33': {} - '@types/ws@8.18.1': dependencies: '@types/node': 25.0.3 @@ -5153,8 +5067,6 @@ snapshots: stubborn-fs: 1.2.5 when-exit: 2.1.4 - balanced-match@1.0.2: {} - base64-js@1.5.1: {} basic-ftp@5.0.5: {} @@ -5184,10 +5096,6 @@ snapshots: widest-line: 5.0.0 wrap-ansi: 9.0.2 - brace-expansion@2.0.2: - dependencies: - balanced-match: 1.0.2 - braces@3.0.3: dependencies: fill-range: 7.1.1 @@ -6115,10 +6023,6 @@ snapshots: - bufferutil - utf-8-validate - minimatch@9.0.5: - dependencies: - brace-expansion: 2.0.2 - minimist@1.2.8: {} module-details-from-path@1.0.4: {} @@ -6488,10 +6392,6 @@ snapshots: semver@7.7.3: {} - serialize-error@8.1.0: - dependencies: - type-fest: 0.20.2 - sharp@0.34.5: dependencies: '@img/colour': 1.0.0 @@ -6562,8 +6462,6 @@ snapshots: sprintf-js@1.0.3: {} - stack-trace@0.0.10: {} - stdin-discarder@0.2.2: {} stoppable@1.1.0: {} @@ -6674,8 +6572,6 @@ snapshots: turbo-windows-64: 2.7.0 turbo-windows-arm64: 2.7.0 - type-fest@0.20.2: {} - type-fest@0.21.3: {} type-fest@4.41.0: {} @@ -6752,7 +6648,7 @@ snapshots: '@cloudflare/workerd-linux-arm64': 1.20260111.0 '@cloudflare/workerd-windows-64': 1.20260111.0 - wrangler@4.59.1(@cloudflare/workers-types@4.20251220.0): + wrangler@4.59.1: dependencies: '@cloudflare/kv-asset-handler': 0.4.1 '@cloudflare/unenv-preset': 2.9.0(unenv@2.0.0-rc.24)(workerd@1.20260111.0) @@ -6763,7 +6659,6 @@ snapshots: unenv: 2.0.0-rc.24 workerd: 1.20260111.0 optionalDependencies: - '@cloudflare/workers-types': 4.20251220.0 fsevents: 2.3.3 transitivePeerDependencies: - bufferutil From 0b4af72fcaa8d7e027ab20c6644df7f63bbbc93d Mon Sep 17 00:00:00 2001 From: ItsRauf <31735267+ItsRauf@users.noreply.github.com> Date: Fri, 13 Feb 2026 15:13:54 -0600 Subject: [PATCH 16/20] =?UTF-8?q?=F0=9F=90=9B=20fix(bot):=20suppress=20deb?= =?UTF-8?q?ug/trace=20console=20spam,=20restore=20console=20output=20for?= =?UTF-8?q?=20info+?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/bot/src/client.ts | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/apps/bot/src/client.ts b/apps/bot/src/client.ts index ff756e5..be32872 100644 --- a/apps/bot/src/client.ts +++ b/apps/bot/src/client.ts @@ -27,16 +27,12 @@ class EmbedlyLogger implements ILogger { } has(level: LogLevel): boolean { - return level >= LogLevel.Debug; + return level >= LogLevel.Info; } - trace(...values: readonly unknown[]) { - this.emit(SeverityNumber.TRACE, "TRACE", values); - } + trace() {} - debug(...values: readonly unknown[]) { - this.emit(SeverityNumber.DEBUG, "DEBUG", values); - } + debug() {} info(...values: readonly unknown[]) { this.emit(SeverityNumber.INFO, "INFO", values); @@ -71,11 +67,25 @@ class EmbedlyLogger implements ILogger { this.emit(num, text, values); } + private static readonly consoleMethods: Record< + string, + (...args: unknown[]) => void + > = { + TRACE: console.trace, + DEBUG: console.debug, + INFO: console.info, + WARN: console.warn, + ERROR: console.error, + FATAL: console.error + }; + private emit( severityNumber: SeverityNumber, severityText: string, values: readonly unknown[] ) { + const consoleMethod = + EmbedlyLogger.consoleMethods[severityText] ?? console.log; const first = values[0]; if ( first && @@ -84,6 +94,7 @@ class EmbedlyLogger implements ILogger { "attributes" in first ) { const log = first as FormattedLog; + consoleMethod(log.body, log.attributes); this.otel.emit({ severityNumber, severityText, @@ -91,6 +102,7 @@ class EmbedlyLogger implements ILogger { attributes: log.attributes }); } else { + consoleMethod(...values); this.otel.emit({ severityNumber, severityText, From 49c02cf04bf93d4f0857dd10cecad28a49e702f1 Mon Sep 17 00:00:00 2001 From: ItsRauf <31735267+ItsRauf@users.noreply.github.com> Date: Fri, 13 Feb 2026 15:22:22 -0600 Subject: [PATCH 17/20] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor(logging):?= =?UTF-8?q?=20move=20EmbedlyLogger=20to=20shared=20logging=20package?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/bot/src/client.ts | 99 +++++------------------------------ packages/logging/package.json | 3 ++ packages/logging/src/main.ts | 72 +++++++++++++++++++++++++ pnpm-lock.yaml | 4 ++ 4 files changed, 91 insertions(+), 87 deletions(-) diff --git a/apps/bot/src/client.ts b/apps/bot/src/client.ts index be32872..765a77f 100644 --- a/apps/bot/src/client.ts +++ b/apps/bot/src/client.ts @@ -1,10 +1,6 @@ -import type { FormattedLog } from "@embedly/logging"; +import { EmbedlyLogger } from "@embedly/logging"; import { type Tracer, trace } from "@opentelemetry/api"; -import { - logs, - type Logger as OtelLogger, - SeverityNumber -} from "@opentelemetry/api-logs"; +import { logs } from "@opentelemetry/api-logs"; import { container, type ILogger, @@ -19,13 +15,7 @@ import { } from "discord.js"; import { PostHog } from "posthog-node"; -class EmbedlyLogger implements ILogger { - private otel: OtelLogger; - - constructor(name: string) { - this.otel = logs.getLogger(name); - } - +class SapphireLogger extends EmbedlyLogger implements ILogger { has(level: LogLevel): boolean { return level >= LogLevel.Info; } @@ -34,80 +24,15 @@ class EmbedlyLogger implements ILogger { debug() {} - info(...values: readonly unknown[]) { - this.emit(SeverityNumber.INFO, "INFO", values); - } - - warn(...values: readonly unknown[]) { - this.emit(SeverityNumber.WARN, "WARN", values); - } - - error(...values: readonly unknown[]) { - this.emit(SeverityNumber.ERROR, "ERROR", values); - } - - fatal(...values: readonly unknown[]) { - this.emit(SeverityNumber.FATAL, "FATAL", values); - } - write(level: LogLevel, ...values: readonly unknown[]) { - const severityMap: Record = { - [LogLevel.Trace]: [SeverityNumber.TRACE, "TRACE"], - [LogLevel.Debug]: [SeverityNumber.DEBUG, "DEBUG"], - [LogLevel.Info]: [SeverityNumber.INFO, "INFO"], - [LogLevel.Warn]: [SeverityNumber.WARN, "WARN"], - [LogLevel.Error]: [SeverityNumber.ERROR, "ERROR"], - [LogLevel.Fatal]: [SeverityNumber.FATAL, "FATAL"], - [LogLevel.None]: [SeverityNumber.UNSPECIFIED, "UNSPECIFIED"] - }; - const [num, text] = severityMap[level] ?? [ - SeverityNumber.INFO, - "INFO" - ]; - this.emit(num, text, values); - } - - private static readonly consoleMethods: Record< - string, - (...args: unknown[]) => void - > = { - TRACE: console.trace, - DEBUG: console.debug, - INFO: console.info, - WARN: console.warn, - ERROR: console.error, - FATAL: console.error - }; - - private emit( - severityNumber: SeverityNumber, - severityText: string, - values: readonly unknown[] - ) { - const consoleMethod = - EmbedlyLogger.consoleMethods[severityText] ?? console.log; - const first = values[0]; - if ( - first && - typeof first === "object" && - "body" in first && - "attributes" in first - ) { - const log = first as FormattedLog; - consoleMethod(log.body, log.attributes); - this.otel.emit({ - severityNumber, - severityText, - body: log.body, - attributes: log.attributes - }); - } else { - consoleMethod(...values); - this.otel.emit({ - severityNumber, - severityText, - body: values.map(String).join(" ") - }); + if (level >= LogLevel.Info) { + const methods = { + [LogLevel.Info]: this.info, + [LogLevel.Warn]: this.warn, + [LogLevel.Error]: this.error, + [LogLevel.Fatal]: this.fatal + } as Record void>; + methods[level]?.call(this, ...values); } } } @@ -145,7 +70,7 @@ export class EmbedlyClient extends SapphireClient { } public override async login(token?: string) { - container.logger = new EmbedlyLogger("embedly-bot"); + container.logger = new SapphireLogger("embedly-bot"); container.embed_authors = new Map(); container.embed_messages = new Map(); container.posthog = new PostHog(process.env.POSTHOG_API_KEY!, { diff --git a/packages/logging/package.json b/packages/logging/package.json index a4dec7d..8cba988 100644 --- a/packages/logging/package.json +++ b/packages/logging/package.json @@ -21,5 +21,8 @@ "@embedly/config": "workspace:*", "pkgroll": "^2.21.4", "typescript": "^5.9.3" + }, + "dependencies": { + "@opentelemetry/api-logs": "^0.211.0" } } diff --git a/packages/logging/src/main.ts b/packages/logging/src/main.ts index de0d6ca..46fa8a2 100644 --- a/packages/logging/src/main.ts +++ b/packages/logging/src/main.ts @@ -221,6 +221,78 @@ export function formatLog( }; } +import { + logs, + type Logger as OtelLogger, + SeverityNumber +} from "@opentelemetry/api-logs"; + +export class EmbedlyLogger { + private otel: OtelLogger; + + constructor(name: string) { + this.otel = logs.getLogger(name); + } + + private static readonly consoleMethods: Record< + string, + (...args: unknown[]) => void + > = { + INFO: console.info, + WARN: console.warn, + ERROR: console.error, + FATAL: console.error + }; + + info(...values: readonly unknown[]) { + this.emit(SeverityNumber.INFO, "INFO", values); + } + + warn(...values: readonly unknown[]) { + this.emit(SeverityNumber.WARN, "WARN", values); + } + + error(...values: readonly unknown[]) { + this.emit(SeverityNumber.ERROR, "ERROR", values); + } + + fatal(...values: readonly unknown[]) { + this.emit(SeverityNumber.FATAL, "FATAL", values); + } + + private emit( + severityNumber: SeverityNumber, + severityText: string, + values: readonly unknown[] + ) { + const consoleMethod = + EmbedlyLogger.consoleMethods[severityText] ?? console.log; + const first = values[0]; + if ( + first && + typeof first === "object" && + "body" in first && + "attributes" in first + ) { + const log = first as FormattedLog; + consoleMethod(log.body, log.attributes); + this.otel.emit({ + severityNumber, + severityText, + body: log.body, + attributes: log.attributes + }); + } else { + consoleMethod(...values); + this.otel.emit({ + severityNumber, + severityText, + body: values.map(String).join(" ") + }); + } + } +} + export function formatDiscord< T extends EmbedlyErrorBase >(err: T, ctx: T["context"]) { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2b58ac2..092b2e8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -226,6 +226,10 @@ importers: version: 2.3.10 packages/logging: + dependencies: + '@opentelemetry/api-logs': + specifier: ^0.211.0 + version: 0.211.0 devDependencies: '@biomejs/biome': specifier: 2.3.10 From 8c899b688f78c653caf973671b7e093b964c6ee2 Mon Sep 17 00:00:00 2001 From: ItsRauf <31735267+ItsRauf@users.noreply.github.com> Date: Fri, 13 Feb 2026 15:35:19 -0600 Subject: [PATCH 18/20] =?UTF-8?q?=F0=9F=94=8A=20feat(logging):=20add=20pla?= =?UTF-8?q?tform=20and=20source=20labels=20to=20log=20attributes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/bot/src/commands/embed.ts | 15 ++++++++++++--- apps/bot/src/listeners/messageCreate.ts | 5 ++++- packages/logging/src/main.ts | 4 ++++ 3 files changed, 20 insertions(+), 4 deletions(-) diff --git a/apps/bot/src/commands/embed.ts b/apps/bot/src/commands/embed.ts index 2b683f0..9dddf15 100644 --- a/apps/bot/src/commands/embed.ts +++ b/apps/bot/src/commands/embed.ts @@ -12,6 +12,7 @@ import { EMBEDLY_NO_VALID_LINK, EMBEDLY_NO_VALID_LINK_WARN, type EmbedlyInteractionContext, + type EmbedlySource, formatDiscord, formatLog } from "@embedly/logging"; @@ -107,11 +108,13 @@ export class EmbedCommand extends Command { | Command.ChatInputCommandInteraction | Command.ContextMenuCommandInteraction, content: string, + source: EmbedlySource, flags?: Partial ) { const log_ctx = { interaction_id: interaction.id, - user_id: interaction.user.id + user_id: interaction.user.id, + source } satisfies EmbedlyInteractionContext; if (!hasLink(content)) { this.container.logger.warn( @@ -188,6 +191,7 @@ export class EmbedCommand extends Command { if (error?.status === 400 || error?.status === 500) { const error_context = { ...log_ctx, + platform: platform.type, ...("context" in error.value ? error.value.context : {}) }; this.container.logger.error( @@ -235,6 +239,7 @@ export class EmbedCommand extends Command { user_id: interaction.user.id, bot_message_id: bot_message.id, platform: platform.type, + source, url }) ); @@ -257,7 +262,11 @@ export class EmbedCommand extends Command { "discord.message_id": msg.id }); try { - await this.fetchEmbed(interaction, msg.content); + await this.fetchEmbed( + interaction, + msg.content, + "context_menu" + ); root_span.setStatus({ code: SpanStatusCode.OK }); } catch (error: any) { root_span.setStatus({ @@ -300,7 +309,7 @@ export class EmbedCommand extends Command { } try { - await this.fetchEmbed(interaction, url, { + await this.fetchEmbed(interaction, url, "command", { [EmbedFlagNames.MediaOnly]: interaction.options.getBoolean("media_only") ?? false, [EmbedFlagNames.SourceOnly]: diff --git a/apps/bot/src/listeners/messageCreate.ts b/apps/bot/src/listeners/messageCreate.ts index 7dfa9fb..b5ba8a9 100644 --- a/apps/bot/src/listeners/messageCreate.ts +++ b/apps/bot/src/listeners/messageCreate.ts @@ -130,7 +130,9 @@ export class MessageListener extends Listener< ? error.value.context : {}), message_id: message.id, - user_id: message.author.id + user_id: message.author.id, + source: "message", + platform: platform.type }; this.container.logger.error( formatLog(error.value, error_context) @@ -205,6 +207,7 @@ export class MessageListener extends Listener< bot_message_id: bot_message.id, user_id: message.author.id, platform: platform.type, + source: "message", url }) ); diff --git a/packages/logging/src/main.ts b/packages/logging/src/main.ts index 46fa8a2..2b7b550 100644 --- a/packages/logging/src/main.ts +++ b/packages/logging/src/main.ts @@ -21,10 +21,14 @@ export const EMBEDLY_INVALID_REQUEST: EmbedlyErrorBase<{ detail: "Failed to validate request from Discord." }; +export type EmbedlySource = "message" | "command" | "context_menu"; + export interface EmbedlyInteractionContext { interaction_id?: string; user_id?: string; message_id?: string; + source?: EmbedlySource; + platform?: PlatformName; } export const EMBEDLY_NO_LINK_IN_MESSAGE: EmbedlyErrorBase = From a1b05b2a59aa844ecf3fb726affdd2bd61e2478d Mon Sep 17 00:00:00 2001 From: ItsRauf <31735267+ItsRauf@users.noreply.github.com> Date: Fri, 13 Feb 2026 16:11:37 -0600 Subject: [PATCH 19/20] =?UTF-8?q?=F0=9F=A9=B9=20fix(api):=20add=20trace/sp?= =?UTF-8?q?an=20IDs=20to=20OTLP=20log=20records=20for=20Tempo=20correlatio?= =?UTF-8?q?n?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/api/src/main.ts | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/apps/api/src/main.ts b/apps/api/src/main.ts index 01dde83..4cb36b6 100644 --- a/apps/api/src/main.ts +++ b/apps/api/src/main.ts @@ -38,6 +38,8 @@ const app = (env: Env, ctx: ExecutionContext) => severityNumber: number; log: FormattedLog; timestamp: number; + traceId: string; + spanId: string; }[] = []; const emit = ( @@ -51,11 +53,15 @@ const app = (env: Env, ctx: ExecutionContext) => | "warn" | "error"; console[method]?.(log.body, log.attributes); + const span = trace.getActiveSpan(); + const spanCtx = span?.spanContext(); pending.push({ severityText, severityNumber, log, - timestamp: Date.now() + timestamp: Date.now(), + traceId: spanCtx?.traceId ?? "", + spanId: spanCtx?.spanId ?? "" }); }; @@ -80,10 +86,14 @@ const app = (env: Env, ctx: ExecutionContext) => severityText, severityNumber, log, - timestamp + timestamp, + traceId, + spanId }) => ({ timeUnixNano: String(timestamp * 1_000_000), severityNumber, + traceId, + spanId, severityText, body: { stringValue: log.body }, attributes: Object.entries(log.attributes) From 6d1f45324094e6a39fceb9ea075e3f5bd93320d0 Mon Sep 17 00:00:00 2001 From: ItsRauf <31735267+ItsRauf@users.noreply.github.com> Date: Fri, 13 Feb 2026 16:13:33 -0600 Subject: [PATCH 20/20] =?UTF-8?q?=F0=9F=A9=B9=20fix(platforms):=20align=20?= =?UTF-8?q?embedly=20version=20for=20fxtwitter=20with=20package.json=20ver?= =?UTF-8?q?sion?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .changeset/six-singers-jog.md | 8 ++++++++ packages/platforms/src/Platform.ts | 2 -- packages/platforms/src/Twitter.ts | 3 ++- 3 files changed, 10 insertions(+), 3 deletions(-) create mode 100644 .changeset/six-singers-jog.md diff --git a/.changeset/six-singers-jog.md b/.changeset/six-singers-jog.md new file mode 100644 index 0000000..24dc567 --- /dev/null +++ b/.changeset/six-singers-jog.md @@ -0,0 +1,8 @@ +--- +"@embedly/platforms": minor +"@embedly/logging": minor +"@embedly/api": minor +"@embedly/bot": minor +--- + +Replace BetterStack (Logtail) with OTEL-native logging for LGTM stack. Logs now flow through OpenTelemetry to Loki alongside traces and metrics. Added EmbedlyLogger class, platform and source labels for Grafana indexing, and trace correlation for the API. diff --git a/packages/platforms/src/Platform.ts b/packages/platforms/src/Platform.ts index 12e7da7..12f87a1 100644 --- a/packages/platforms/src/Platform.ts +++ b/packages/platforms/src/Platform.ts @@ -21,8 +21,6 @@ export interface CloudflareEnv { EMBED_USER_AGENT: string; THREADS_CSRF_TOKEN?: string; STORAGE: KVNamespace; - BETTERSTACK_SOURCE_TOKEN: string; - BETTERSTACK_INGESTING_HOST: string; DISCORD_BOT_TOKEN: string; } diff --git a/packages/platforms/src/Twitter.ts b/packages/platforms/src/Twitter.ts index 07f01eb..4dd43a9 100644 --- a/packages/platforms/src/Twitter.ts +++ b/packages/platforms/src/Twitter.ts @@ -1,5 +1,6 @@ import { Embed } from "@embedly/builder"; import he from "he"; +import packageJSON from "../package.json" with { type: "json" }; import { CF_CACHE_OPTIONS } from "./constants.ts"; import { type BaseEmbedData, EmbedlyPlatform } from "./Platform.ts"; import { EmbedlyPlatformType } from "./types.ts"; @@ -31,7 +32,7 @@ export class Twitter extends EmbedlyPlatform { { method: "GET", headers: { - "User-Agent": "Embedly/0.0.1" + "User-Agent": `Embedly/${packageJSON.version}` }, ...CF_CACHE_OPTIONS }