From 74ff0a20eafd82c696705e74a0d1addee39b3151 Mon Sep 17 00:00:00 2001 From: Cody Date: Tue, 12 May 2026 06:43:14 -0600 Subject: [PATCH 01/11] feat: instrument command latency in sentry --- apps/api/package.json | 1 + apps/api/src/hypixel/hypixel.service.ts | 16 +++-- apps/api/src/index.ts | 10 ++- apps/api/src/redis/redis.utils.ts | 18 +++++- apps/discord-bot/package.json | 1 + .../src/commands/base.hypixel-command.ts | 1 + .../src/commands/ratios/ratios.command.tsx | 18 +++++- apps/discord-bot/src/index.ts | 17 ++++- apps/discord-bot/src/lib/command.listener.ts | 21 ++++++- packages/api-client/src/api.service.ts | 41 ++++++++---- .../src/command/abstract-command.listener.ts | 33 ++++++++-- .../discord/src/command/command.interface.ts | 5 ++ .../discord/src/command/command.resolvable.ts | 3 + .../discord/src/interaction/interaction.ts | 15 ++++- .../discord/src/services/paginate.service.ts | 1 + packages/rendering/src/jsx/render.ts | 7 ++- packages/util/src/config.ts | 5 ++ pnpm-lock.yaml | 63 ++++++++++++++++--- 18 files changed, 234 insertions(+), 42 deletions(-) diff --git a/apps/api/package.json b/apps/api/package.json index 81d7ec0c9..a4ab97355 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -21,6 +21,7 @@ "@nestjs/platform-fastify": "^11.1.6", "@nestjs/swagger": "^11.2.0", "@sentry/node": "^7.118.0", + "@sentry/profiling-node": "^7.120.4", "@statsify/api-client": "workspace:^", "@statsify/assets": "workspace:^", "@statsify/logger": "workspace:^", diff --git a/apps/api/src/hypixel/hypixel.service.ts b/apps/api/src/hypixel/hypixel.service.ts index 426ad3e22..0c5694f39 100644 --- a/apps/api/src/hypixel/hypixel.service.ts +++ b/apps/api/src/hypixel/hypixel.service.ts @@ -145,8 +145,12 @@ export class HypixelService { const transaction = Sentry.getCurrentHub().getScope()?.getTransaction(); const child = transaction?.startChild({ - op: "http.client", + op: "hypixel.api.fetch", description: `GET ${this.httpService.axiosRef.getUri({ url })}`, + data: { + "http.method": "GET", + "http.route": url, + }, }); return this.httpService.get(url, { params }).pipe( @@ -155,14 +159,16 @@ export class HypixelService { child?.finish(); }), map((res) => res.data), - catchError((err) => - throwError( + catchError((err) => { + child?.finish(); + + return throwError( () => new Error(`Fetching ${url} failed with reason: ${err.message}`, { cause: err, }) - ) - ) + ); + }) ); } } diff --git a/apps/api/src/index.ts b/apps/api/src/index.ts index 648d33c1a..e3165ad8b 100644 --- a/apps/api/src/index.ts +++ b/apps/api/src/index.ts @@ -20,6 +20,7 @@ import { ValidationPipe } from "@nestjs/common"; import { config } from "@statsify/util"; import { join } from "node:path"; import { mkdir } from "node:fs/promises"; +import { nodeProfilingIntegration } from "@sentry/profiling-node"; const directory = import.meta.dirname; @@ -30,6 +31,11 @@ process.on("uncaughtException", handleError); process.on("unhandledRejection", handleError); const sentryDsn = await config("sentry.apiDsn", { required: false }); +const sentryTracesSampleRate = + await config("sentry.tracesSampleRate", { required: false }) ?? 0; +const sentryProfilesSampleRate = + await config("sentry.profilesSampleRate", { required: false }) ?? + sentryTracesSampleRate; if (sentryDsn) { Sentry.init({ @@ -37,9 +43,11 @@ if (sentryDsn) { integrations: [ new Sentry.Integrations.Http({ tracing: false, breadcrumbs: true }), new Sentry.Integrations.Mongo({ useMongoose: true }), + nodeProfilingIntegration(), ], normalizeDepth: 3, - tracesSampleRate: await config("sentry.tracesSampleRate"), + tracesSampleRate: sentryTracesSampleRate, + profilesSampleRate: sentryProfilesSampleRate, environment: await config("environment"), }); } diff --git a/apps/api/src/redis/redis.utils.ts b/apps/api/src/redis/redis.utils.ts index 8e57a607e..008980f08 100644 --- a/apps/api/src/redis/redis.utils.ts +++ b/apps/api/src/redis/redis.utils.ts @@ -6,6 +6,7 @@ * https://github.com/Statsify/statsify/blob/main/LICENSE */ +import * as Sentry from "@sentry/node"; import { REDIS_MODULE_CONNECTION, REDIS_MODULE_CONNECTION_TOKEN, @@ -24,5 +25,20 @@ export function getRedisConnectionToken(connection?: string): string { export function createRedisConnection(options: RedisModuleOptions) { const { config } = options; - return config.url ? new Redis(config.url, config) : new Redis(config); + const redis = config.url ? new Redis(config.url, config) : new Redis(config); + const sendCommand = redis.sendCommand.bind(redis); + + redis.sendCommand = ((command, stream) => { + const commandName = String((command as { name: string }).name); + const transaction = Sentry.getCurrentHub().getScope()?.getTransaction(); + const span = transaction?.startChild({ + op: "redis.query", + description: commandName, + data: { "redis.command": commandName }, + }); + + return (sendCommand(command, stream) as Promise).finally(() => span?.finish()); + }) as Redis["sendCommand"]; + + return redis; } diff --git a/apps/discord-bot/package.json b/apps/discord-bot/package.json index 7fb74f2e5..55a856b5d 100644 --- a/apps/discord-bot/package.json +++ b/apps/discord-bot/package.json @@ -13,6 +13,7 @@ }, "dependencies": { "@sentry/node": "^7.118.0", + "@sentry/profiling-node": "^7.120.4", "@statsify/api-client": "workspace:^", "@statsify/assets": "workspace:^", "@statsify/discord": "workspace:^", diff --git a/apps/discord-bot/src/commands/base.hypixel-command.ts b/apps/discord-bot/src/commands/base.hypixel-command.ts index 3d32a4f2e..6b3dd96c9 100644 --- a/apps/discord-bot/src/commands/base.hypixel-command.ts +++ b/apps/discord-bot/src/commands/base.hypixel-command.ts @@ -59,6 +59,7 @@ export interface BaseHypixelCommand { description: "", args: [PlayerArgument], cooldown: 10, + group: "hypixel", }) export abstract class BaseHypixelCommand { protected readonly apiService: ApiService; diff --git a/apps/discord-bot/src/commands/ratios/ratios.command.tsx b/apps/discord-bot/src/commands/ratios/ratios.command.tsx index 854c04db9..45755e88a 100644 --- a/apps/discord-bot/src/commands/ratios/ratios.command.tsx +++ b/apps/discord-bot/src/commands/ratios/ratios.command.tsx @@ -6,6 +6,7 @@ * https://github.com/Statsify/statsify/blob/main/LICENSE */ +import * as Sentry from "@sentry/node"; import { ARCADE_MODES, ARENA_BRAWL_MODES, @@ -57,7 +58,7 @@ import { render } from "@statsify/rendering"; const args = [PlayerArgument]; -@Command({ description: (t) => t("commands.ratios") }) +@Command({ description: (t) => t("commands.ratios"), group: "hypixel" }) export class RatiosCommand { public constructor(private readonly apiService: ApiService) {} @@ -232,7 +233,20 @@ export class RatiosCommand { }; const canvas = render(, getTheme(user)); - const buffer = await canvas.toBuffer("png"); + const transaction = Sentry.getCurrentHub().getScope()?.getTransaction(); + const span = transaction?.startChild({ + op: "canvas.encode_png", + description: "Encode ratios canvas as PNG", + }); + + let buffer: Buffer; + + try { + buffer = await canvas.toBuffer("png"); + span?.setData("png.bytes", buffer.byteLength); + } finally { + span?.finish(); + } return { files: [{ name: "ratios.png", data: buffer, type: "image/png" }], diff --git a/apps/discord-bot/src/index.ts b/apps/discord-bot/src/index.ts index 7bfbafbb6..b285cd91c 100644 --- a/apps/discord-bot/src/index.ts +++ b/apps/discord-bot/src/index.ts @@ -15,7 +15,9 @@ import { InteractionServer, RestClient, WebsocketShard } from "tiny-discord"; import { Logger } from "@statsify/logger"; import { VerifyCommand } from "#commands/verify.command"; import { config } from "@statsify/util"; -import { join } from "node:path"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; +import { nodeProfilingIntegration } from "@sentry/profiling-node"; const directory = import.meta.dirname; @@ -26,13 +28,22 @@ process.on("uncaughtException", handleError); process.on("unhandledRejection", handleError); const sentryDsn = await config("sentry.discordBotDsn", { required: false }); +const sentryTracesSampleRate = + await config("sentry.tracesSampleRate", { required: false }) ?? 0; +const sentryProfilesSampleRate = + await config("sentry.profilesSampleRate", { required: false }) ?? + sentryTracesSampleRate; if (sentryDsn) { Sentry.init({ dsn: sentryDsn, - integrations: [new Sentry.Integrations.Http({ tracing: false, breadcrumbs: true })], + integrations: [ + new Sentry.Integrations.Http({ tracing: false, breadcrumbs: true }), + nodeProfilingIntegration(), + ], normalizeDepth: 3, - tracesSampleRate: await config("sentry.tracesSampleRate"), + tracesSampleRate: sentryTracesSampleRate, + profilesSampleRate: sentryProfilesSampleRate, environment: await config("environment"), }); } diff --git a/apps/discord-bot/src/lib/command.listener.ts b/apps/discord-bot/src/lib/command.listener.ts index d42bbf4f6..43491a9d9 100644 --- a/apps/discord-bot/src/lib/command.listener.ts +++ b/apps/discord-bot/src/lib/command.listener.ts @@ -65,12 +65,31 @@ export class CommandListener extends AbstractCommandListener { parentData ); - const transaction = Sentry.startTransaction({ name: commandName, op: "command" }); + const [name, ...subcommandParts] = commandName.split(" "); + const subcommand = subcommandParts.length ? commandName : undefined; + + const transaction = Sentry.startTransaction({ + name: commandName, + op: "discord.command.total", + data: { + "command.name": name, + "command.group": parentCommand.group ?? command.group ?? "unknown", + "command.subcommand": subcommand, + }, + tags: { + "command.name": name, + "command.group": parentCommand.group ?? command.group ?? "unknown", + "command.subcommand": subcommand ?? "none", + }, + }); Sentry.configureScope((scope) => scope.setSpan(transaction)); Sentry.setContext("command", { command: commandName, + group: parentCommand.group ?? command.group ?? null, + name, + subcommand: subcommand ?? null, options: data.options, guild: interaction.getGuildId() ?? null, }); diff --git a/packages/api-client/src/api.service.ts b/packages/api-client/src/api.service.ts index f43108367..126f1a644 100644 --- a/packages/api-client/src/api.service.ts +++ b/packages/api-client/src/api.service.ts @@ -7,7 +7,13 @@ */ import * as Sentry from "@sentry/node"; -import axios, { AxiosInstance, AxiosRequestHeaders, Method, ResponseType } from "axios"; +import Axios, { + AxiosInstance, + AxiosRequestHeaders, + AxiosResponse, + Method, + ResponseType, +} from "axios"; import { CacheLevel, GuildQuery, @@ -291,21 +297,30 @@ export class ApiService { const transaction = Sentry.getCurrentHub().getScope()?.getTransaction(); const child = transaction?.startChild({ - op: "http.client", + op: "statsify.api.fetch", description: `${method} ${url}`, + data: { + "http.method": method, + "http.route": url, + }, }); - const res = await this.axios.request({ - url, - method, - params, - headers, - data: body, - responseType, - }); - - child?.setHttpStatus(res.status); - child?.finish(); + let res: AxiosResponse; + + try { + res = await this.axios.request({ + url, + method, + params, + headers, + data: body, + responseType, + }); + + child?.setHttpStatus(res.status); + } finally { + child?.finish(); + } const data = res.data; diff --git a/packages/discord/src/command/abstract-command.listener.ts b/packages/discord/src/command/abstract-command.listener.ts index 90527fdb1..fce60d26e 100644 --- a/packages/discord/src/command/abstract-command.listener.ts +++ b/packages/discord/src/command/abstract-command.listener.ts @@ -130,6 +130,10 @@ export abstract class AbstractCommandListener { message, }: ExecuteCommandOptions) { const transaction = Sentry.getCurrentHub().getScope()?.getTransaction(); + const commandSpan = transaction?.startChild({ + op: "discord.command.execute", + description: commandName, + }); try { for (const precondition of preconditions) { @@ -137,22 +141,34 @@ export abstract class AbstractCommandListener { } const response = await command.execute(context); + commandSpan?.finish(); - if (typeof response !== "object") return; - transaction?.finish(); + if (typeof response !== "object") { + this.setMemoryUsage(transaction); + transaction?.finish(); + return; + } - context.reply({ + await context.reply({ ...message, ...response, }); + + this.setMemoryUsage(transaction); + transaction?.finish(); } catch (err) { if (err instanceof Message) { + commandSpan?.finish(); + await context.reply(err); + this.setMemoryUsage(transaction); transaction?.finish(); - return context.reply(err); + return; } this.logger.error(`An error occurred when running "${commandName}"`); this.logger.error(err); + commandSpan?.finish(); + this.setMemoryUsage(transaction); transaction?.finish(); } } @@ -319,5 +335,14 @@ export abstract class AbstractCommandListener { }); } + private setMemoryUsage(transaction?: Sentry.Transaction) { + if (!transaction) return; + + const { rss, heapUsed } = process.memoryUsage(); + + transaction.setData("memory.rss.bytes", rss); + transaction.setData("memory.heap_used.bytes", heapUsed); + } + protected abstract onCommand(interaction: Interaction): Promise | void; } diff --git a/packages/discord/src/command/command.interface.ts b/packages/discord/src/command/command.interface.ts index 84ff30af1..da94e8e0c 100644 --- a/packages/discord/src/command/command.interface.ts +++ b/packages/discord/src/command/command.interface.ts @@ -27,6 +27,11 @@ export interface CommandOptions { cooldown?: number; + /** + * The product area this command belongs to. Used for observability. + */ + group?: string; + /** * The minimum user tier required to use this command. */ diff --git a/packages/discord/src/command/command.resolvable.ts b/packages/discord/src/command/command.resolvable.ts index 5279bae4c..f7b0431dc 100644 --- a/packages/discord/src/command/command.resolvable.ts +++ b/packages/discord/src/command/command.resolvable.ts @@ -33,6 +33,7 @@ export class CommandResolvable { public args: AbstractArgument[]; public cooldown: number; + public group?: string; public tier: UserTier; public preview?: string; @@ -49,6 +50,7 @@ export class CommandResolvable { methodName, tier = UserTier.NONE, preview, + group, cooldown = 10, }: CommandMetadata, target: any @@ -72,6 +74,7 @@ export class CommandResolvable { this.type = ApplicationCommandType.ChatInput; this.cooldown = cooldown; + this.group = group; const argsResolved = (args ?? [])?.map((a) => a instanceof AbstractArgument ? a : new a() diff --git a/packages/discord/src/interaction/interaction.ts b/packages/discord/src/interaction/interaction.ts index 6707683f1..5aadf84c5 100644 --- a/packages/discord/src/interaction/interaction.ts +++ b/packages/discord/src/interaction/interaction.ts @@ -6,6 +6,7 @@ * https://github.com/Statsify/statsify/blob/main/LICENSE */ +import * as Sentry from "@sentry/node"; import { type APIGuildMember, type APIUser, @@ -154,7 +155,17 @@ export class Interaction { } private async request(options: RestClient.RequestOptions) { - const response = await this.rest.request(options); - return parseDiscordResponse(response); + const transaction = Sentry.getCurrentHub().getScope()?.getTransaction(); + const span = transaction?.startChild({ + op: "discord.reply", + description: `${options.method.toUpperCase()} ${options.path}`, + }); + + try { + const response = await this.rest.request(options); + return parseDiscordResponse(response); + } finally { + span?.finish(); + } } } diff --git a/packages/discord/src/services/paginate.service.ts b/packages/discord/src/services/paginate.service.ts index ac7295d12..e7aae18b9 100644 --- a/packages/discord/src/services/paginate.service.ts +++ b/packages/discord/src/services/paginate.service.ts @@ -6,6 +6,7 @@ * https://github.com/Statsify/statsify/blob/main/LICENSE */ +import * as Sentry from "@sentry/node"; import { ButtonStyle } from "discord-api-types/v10"; import { Canvas } from "skia-canvas"; diff --git a/packages/rendering/src/jsx/render.ts b/packages/rendering/src/jsx/render.ts index b1e3a1e1b..4eba53de4 100644 --- a/packages/rendering/src/jsx/render.ts +++ b/packages/rendering/src/jsx/render.ts @@ -122,7 +122,7 @@ export function render(node: ElementNode, theme?: Theme): Canvas { const transaction = Sentry.getCurrentHub().getScope()?.getTransaction(); const instructionsTransaction = transaction?.startChild({ - op: "jsx.createInstructions", + op: "render.create_instructions", description: "Create instructions", }); @@ -134,8 +134,9 @@ export function render(node: ElementNode, theme?: Theme): Canvas { const height = Math.round(getTotalSize(instructions.y)); const renderTransaction = transaction?.startChild({ - op: "jsx.render", - description: "Render JSX", + op: "render.generate", + description: "Generate render canvas", + data: { width, height }, }); const canvas = createCanvas(width, height); diff --git a/packages/util/src/config.ts b/packages/util/src/config.ts index b3c71297e..f15a58255 100644 --- a/packages/util/src/config.ts +++ b/packages/util/src/config.ts @@ -228,6 +228,11 @@ export interface Config { * The percentage of transactions to send to Sentry */ tracesSampleRate?: number; + + /** + * The percentage of sampled transactions to profile with Sentry + */ + profilesSampleRate?: number; }; /** diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 183491aaa..50476687e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -89,6 +89,9 @@ importers: '@sentry/node': specifier: ^7.118.0 version: 7.120.4 + '@sentry/profiling-node': + specifier: ^7.120.4 + version: 7.120.4 '@statsify/api-client': specifier: workspace:^ version: link:../../packages/api-client @@ -162,6 +165,9 @@ importers: '@sentry/node': specifier: ^7.118.0 version: 7.120.4 + '@sentry/profiling-node': + specifier: ^7.120.4 + version: 7.120.4 '@statsify/api-client': specifier: workspace:^ version: link:../../packages/api-client @@ -648,6 +654,8 @@ importers: packages/skin-renderer/pkg: {} + packages/skin-renderer/pkg: {} + packages/util: dependencies: '@swc/helpers': @@ -2690,6 +2698,11 @@ packages: resolution: {integrity: sha512-qq3wZAXXj2SRWhqErnGCSJKUhPSlZ+RGnCZjhfjHpP49KNpcd9YdPTIUsFMgeyjdh6Ew6aVCv23g1hTP0CHpYw==} engines: {node: '>=8'} + '@sentry/profiling-node@7.120.4': + resolution: {integrity: sha512-2Eb/LcYk7ohUx1KNnxcrN6hiyFTbD8Q9ffAvqtx09yJh1JhasvA+XCAcY72ONI5Aia4rCVkql9eEPSyhkmhsbA==} + engines: {node: '>=8.0.0'} + hasBin: true + '@sentry/types@7.120.4': resolution: {integrity: sha512-cUq2hSSe6/qrU6oZsEP4InMI5VVdD86aypE+ENrQ6eZEVLTCYm1w6XhW1NvIu3UuWh7gZec4a9J7AFpYxki88Q==} engines: {node: '>=8'} @@ -4745,6 +4758,10 @@ packages: sass: optional: true + node-abi@3.92.0: + resolution: {integrity: sha512-KdHvFWZjEKDf0cakgFjebl371GPsISX2oZHcuyKqM7DtogIsHrqKeLTo8wBHxaXRAQlY2PsPlZmfo+9ZCxEREQ==} + engines: {node: '>=10'} + node-fetch@2.7.0: resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} engines: {node: 4.x || >=6.0.0} @@ -7463,6 +7480,11 @@ snapshots: '@sentry/types': 7.120.4 '@sentry/utils': 7.120.4 + '@sentry/profiling-node@7.120.4': + dependencies: + detect-libc: 2.1.2 + node-abi: 3.92.0 + '@sentry/types@7.120.4': {} '@sentry/utils@7.120.4': @@ -7784,9 +7806,9 @@ snapshots: fast-glob: 3.3.3 is-glob: 4.0.3 minimatch: 9.0.5 - semver: 7.7.2 - ts-api-utils: 2.1.0(typescript@6.0.3) - typescript: 6.0.3 + semver: 7.7.3 + ts-api-utils: 2.1.0(typescript@5.9.2) + typescript: 5.9.2 transitivePeerDependencies: - supports-color @@ -9014,6 +9036,21 @@ snapshots: dependencies: binary-extensions: 2.3.0 + is-boolean-object@1.2.2: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-builtin-module@5.0.0: + dependencies: + builtin-modules: 5.0.0 + + is-bun-module@2.0.0: + dependencies: + semver: 7.7.3 + + is-callable@1.2.7: {} + is-class@0.0.9: {} is-core-module@2.16.1: @@ -9114,7 +9151,14 @@ snapshots: lodash.isstring: 4.0.1 lodash.once: 4.1.1 ms: 2.1.3 - semver: 7.8.1 + semver: 7.7.3 + + jsx-ast-utils@3.3.5: + dependencies: + array-includes: 3.1.9 + array.prototype.flat: 1.3.3 + object.assign: 4.1.7 + object.values: 1.2.1 jwa@1.4.2: dependencies: @@ -9473,6 +9517,10 @@ snapshots: - '@babel/core' - babel-plugin-macros + node-abi@3.92.0: + dependencies: + semver: 7.7.3 + node-fetch@2.7.0: dependencies: whatwg-url: 5.0.0 @@ -10029,7 +10077,9 @@ snapshots: semver-truncate@3.0.0: dependencies: - semver: 7.8.1 + semver: 7.7.3 + + semver@6.3.1: {} semver@7.5.4: dependencies: @@ -10037,8 +10087,7 @@ snapshots: semver@7.7.2: {} - semver@7.7.3: - optional: true + semver@7.7.3: {} semver@7.8.1: {} From 20366c8bac66251de4076177f304620952ca65d7 Mon Sep 17 00:00:00 2001 From: Cody Date: Sat, 30 May 2026 14:26:04 -0600 Subject: [PATCH 02/11] refactor: clean up sentry command tracing --- apps/api/src/hypixel/hypixel.service.ts | 31 +++---- apps/api/src/index.ts | 4 +- .../src/leaderboards/leaderboard.service.ts | 43 ++++----- apps/api/src/redis/redis.utils.ts | 53 +++++++++-- apps/api/src/sentry/index.ts | 1 + apps/api/src/sentry/mongoose.ts | 93 +++++++++++++++++++ .../src/commands/ratios/ratios.command.tsx | 16 +--- apps/discord-bot/src/lib/command.listener.ts | 47 +++++----- assets/private | 2 +- packages/api-client/package.json | 1 + packages/api-client/src/api.service.ts | 24 ++--- .../src/command/abstract-command.listener.ts | 27 ++---- .../discord/src/interaction/interaction.ts | 34 +++++-- .../discord/src/services/paginate.service.ts | 1 - packages/logger/src/index.ts | 64 +++++++++++++ packages/rendering/package.json | 1 + packages/rendering/src/canvas.ts | 53 +++++++++++ packages/rendering/src/jsx/render.ts | 65 ++++++------- pnpm-lock.yaml | 6 ++ 19 files changed, 396 insertions(+), 170 deletions(-) create mode 100644 apps/api/src/sentry/mongoose.ts diff --git a/apps/api/src/hypixel/hypixel.service.ts b/apps/api/src/hypixel/hypixel.service.ts index 0c5694f39..84aec794d 100644 --- a/apps/api/src/hypixel/hypixel.service.ts +++ b/apps/api/src/hypixel/hypixel.service.ts @@ -6,7 +6,6 @@ * https://github.com/Statsify/statsify/blob/main/LICENSE */ -import * as Sentry from "@sentry/node"; import { CacheLevel } from "@statsify/api-client"; import { GameCounts, @@ -18,8 +17,8 @@ import { } from "@statsify/schemas"; import { HttpService } from "@nestjs/axios"; import { Injectable } from "@nestjs/common"; -import { Logger } from "@statsify/logger"; -import { Observable, catchError, lastValueFrom, map, of, tap, throwError } from "rxjs"; +import { Logger, startSentrySpan } from "@statsify/logger"; +import { Observable, catchError, finalize, lastValueFrom, map, of, tap, throwError } from "rxjs"; import type { APIData } from "@statsify/util"; @Injectable() @@ -142,10 +141,8 @@ export class HypixelService { } private request(url: string, params?: Record): Observable { - const transaction = Sentry.getCurrentHub().getScope()?.getTransaction(); - - const child = transaction?.startChild({ - op: "hypixel.api.fetch", + const span = startSentrySpan({ + op: "hypixel.fetch", description: `GET ${this.httpService.axiosRef.getUri({ url })}`, data: { "http.method": "GET", @@ -155,20 +152,16 @@ export class HypixelService { return this.httpService.get(url, { params }).pipe( tap((res) => { - child?.setHttpStatus(res.status); - child?.finish(); + span?.setHttpStatus(res.status); }), map((res) => res.data), - catchError((err) => { - child?.finish(); - - return throwError( - () => - new Error(`Fetching ${url} failed with reason: ${err.message}`, { - cause: err, - }) - ); - }) + catchError((err) => throwError( + () => + new Error(`Fetching ${url} failed with reason: ${err.message}`, { + cause: err, + }) + )), + finalize(() => span?.finish()) ); } } diff --git a/apps/api/src/index.ts b/apps/api/src/index.ts index e3165ad8b..3e6694d19 100644 --- a/apps/api/src/index.ts +++ b/apps/api/src/index.ts @@ -14,7 +14,7 @@ import { DocumentBuilder, SwaggerModule } from "@nestjs/swagger"; import { FastifyAdapter, NestFastifyApplication } from "@nestjs/platform-fastify"; import { Logger } from "@statsify/logger"; import { NestFactory } from "@nestjs/core"; -import { SentryInterceptor } from "./sentry/index.js"; +import { SentryInterceptor, instrumentMongooseQueries } from "./sentry/index.js"; import { Severity, setGlobalOptions } from "@typegoose/typegoose"; import { ValidationPipe } from "@nestjs/common"; import { config } from "@statsify/util"; @@ -52,6 +52,8 @@ if (sentryDsn) { }); } +instrumentMongooseQueries(); + const mediaRoot = await config("api.mediaRoot"); await mkdir(join(mediaRoot, "badges"), { recursive: true }); diff --git a/apps/api/src/leaderboards/leaderboard.service.ts b/apps/api/src/leaderboards/leaderboard.service.ts index 58a15af98..d3327da08 100644 --- a/apps/api/src/leaderboards/leaderboard.service.ts +++ b/apps/api/src/leaderboards/leaderboard.service.ts @@ -6,7 +6,6 @@ * https://github.com/Statsify/statsify/blob/main/LICENSE */ -import * as Sentry from "@sentry/node"; import { Constructor, Flatten } from "@statsify/util"; import { DateTime } from "luxon"; import { InjectRedis } from "#redis"; @@ -14,6 +13,7 @@ import { Injectable, InternalServerErrorException } from "@nestjs/common"; import { type LeaderboardEnabledMetadata, getLeaderboardField, getLeaderboardFields } from "@statsify/schemas"; import { LeaderboardQuery } from "@statsify/api-client"; import { Redis } from "ioredis"; +import { withSentrySpan } from "@statsify/logger"; const DAYS_IN_WEEK = { monday: 0, @@ -67,9 +67,10 @@ export abstract class LeaderboardService { } } - await pipeline.exec(); - - child?.finish(); + await withSentrySpan({ + op: "redis.write", + description: `add ${constructor.name} leaderboards`, + }, () => pipeline.exec()); } public async getLeaderboard( @@ -194,13 +195,6 @@ export abstract class LeaderboardService { fields: string[], id: string ) { - const transaction = Sentry.getCurrentHub().getScope()?.getTransaction(); - - const child = transaction?.startChild({ - op: "redis", - description: `get ${constructor.name} rankings`, - }); - const pipeline = this.redis.pipeline(); const constructorName = constructor.name.toLowerCase(); @@ -221,9 +215,10 @@ export abstract class LeaderboardService { } } - const responses = await pipeline.exec(); - - child?.finish(); + const responses = await withSentrySpan({ + op: "redis.get", + description: `get ${constructor.name} rankings`, + }, () => pipeline.exec()); if (!responses) throw new InternalServerErrorException(); @@ -274,21 +269,17 @@ export abstract class LeaderboardService { bottom: number, sort = "DESC" ) { - const transaction = Sentry.getCurrentHub().getScope()?.getTransaction(); - - const child = transaction?.startChild({ - op: "redis", - description: `get ${constructor.name} leaderboards`, - }); - const name = constructor.name.toLowerCase(); field = `${name}.${field}`; - const scores = await (sort === "ASC" ? - this.redis.zrange(field, top, bottom, "WITHSCORES") : - this.redis.zrevrange(field, top, bottom, "WITHSCORES")); - - child?.finish(); + const scores = await withSentrySpan({ + op: "redis.get", + description: `get ${constructor.name} leaderboards`, + }, () => + sort === "ASC" ? + this.redis.zrange(field, top, bottom, "WITHSCORES") : + this.redis.zrevrange(field, top, bottom, "WITHSCORES") + ); const response: { id: string; score: number; index: number }[] = []; diff --git a/apps/api/src/redis/redis.utils.ts b/apps/api/src/redis/redis.utils.ts index 008980f08..74490c286 100644 --- a/apps/api/src/redis/redis.utils.ts +++ b/apps/api/src/redis/redis.utils.ts @@ -6,15 +6,44 @@ * https://github.com/Statsify/statsify/blob/main/LICENSE */ -import * as Sentry from "@sentry/node"; import { REDIS_MODULE_CONNECTION, REDIS_MODULE_CONNECTION_TOKEN, REDIS_MODULE_OPTIONS_TOKEN, } from "./redis.constants.js"; import { Redis } from "ioredis"; +import { startSentrySpan } from "@statsify/logger"; import type { RedisModuleOptions } from "./redis.interfaces.js"; +const REDIS_READ_COMMANDS = new Set([ + "exists", + "get", + "hget", + "hgetall", + "hmget", + "mget", + "ttl", + "zrank", + "zrange", + "zrevrank", + "zrevrange", + "zscore", + "ft.sugget", +]); + +const REDIS_WRITE_COMMANDS = new Set([ + "del", + "expire", + "expireat", + "hset", + "hmset", + "set", + "zadd", + "zrem", + "ft.sugadd", + "ft.sugdel", +]); + export function getRedisOptionsToken(connection?: string): string { return `${connection || REDIS_MODULE_CONNECTION}_${REDIS_MODULE_OPTIONS_TOKEN}`; } @@ -29,16 +58,28 @@ export function createRedisConnection(options: RedisModuleOptions) { const sendCommand = redis.sendCommand.bind(redis); redis.sendCommand = ((command, stream) => { - const commandName = String((command as { name: string }).name); - const transaction = Sentry.getCurrentHub().getScope()?.getTransaction(); - const span = transaction?.startChild({ - op: "redis.query", + const commandName = String((command as { name: string }).name).toLowerCase(); + const span = startSentrySpan({ + op: getRedisSpanOperation(commandName), description: commandName, data: { "redis.command": commandName }, }); - return (sendCommand(command, stream) as Promise).finally(() => span?.finish()); + try { + return (sendCommand(command, stream) as Promise).finally(() => + span?.finish() + ); + } catch (error) { + span?.finish(); + throw error; + } }) as Redis["sendCommand"]; return redis; } + +function getRedisSpanOperation(commandName: string) { + if (REDIS_READ_COMMANDS.has(commandName)) return "redis.get"; + if (REDIS_WRITE_COMMANDS.has(commandName)) return "redis.write"; + return "redis.command"; +} diff --git a/apps/api/src/sentry/index.ts b/apps/api/src/sentry/index.ts index 0bb0a391a..5343cd72e 100644 --- a/apps/api/src/sentry/index.ts +++ b/apps/api/src/sentry/index.ts @@ -7,3 +7,4 @@ */ export * from "./sentry.interceptor.js"; +export * from "./mongoose.js"; diff --git a/apps/api/src/sentry/mongoose.ts b/apps/api/src/sentry/mongoose.ts new file mode 100644 index 000000000..ccf6adae6 --- /dev/null +++ b/apps/api/src/sentry/mongoose.ts @@ -0,0 +1,93 @@ +/** + * Copyright (c) Statsify + * + * This source code is licensed under the GNU GPL v3 license found in the + * LICENSE file in the root directory of this source tree. + * https://github.com/Statsify/statsify/blob/main/LICENSE + */ + +import { Aggregate, Query } from "mongoose"; +import { startSentrySpan } from "@statsify/logger"; + +let mongooseInstrumented = false; + +type InstrumentedQuery = Query & { + mongooseCollection?: { name?: string }; + op?: string; +}; + +type InstrumentedAggregate = Aggregate & { + _model?: { + collection?: { name?: string }; + modelName?: string; + }; +}; + +export function instrumentMongooseQueries() { + if (mongooseInstrumented) return; + mongooseInstrumented = true; + + instrumentQueryExec(); + instrumentAggregateExec(); +} + +function instrumentQueryExec() { + const exec = Query.prototype.exec; + + Query.prototype.exec = function instrumentedExec( + this: InstrumentedQuery, + ...args: Parameters + ): ReturnType { + const collection = this.mongooseCollection?.name ?? this.model.collection.name; + const operation = this.op ?? "query"; + const span = startSentrySpan({ + op: "mongo.query", + description: `${collection}.${operation}`, + data: { + "db.collection": collection, + "db.operation": operation, + "db.system": "mongodb", + "mongoose.model": this.model.modelName, + }, + }); + + try { + return exec.apply(this, args).finally(() => span?.finish()) as ReturnType< + typeof exec + >; + } catch (error) { + span?.finish(); + throw error; + } + }; +} + +function instrumentAggregateExec() { + const exec = Aggregate.prototype.exec; + + Aggregate.prototype.exec = function instrumentedExec( + this: InstrumentedAggregate, + ...args: Parameters + ): ReturnType { + const collection = this._model?.collection?.name ?? "unknown"; + const span = startSentrySpan({ + op: "mongo.query", + description: `${collection}.aggregate`, + data: { + "db.collection": collection, + "db.operation": "aggregate", + "db.system": "mongodb", + "mongoose.model": this._model?.modelName ?? "unknown", + }, + }); + + try { + return exec.apply(this, args).finally(() => span?.finish()) as ReturnType< + typeof exec + >; + } catch (error) { + span?.finish(); + throw error; + } + }; +} diff --git a/apps/discord-bot/src/commands/ratios/ratios.command.tsx b/apps/discord-bot/src/commands/ratios/ratios.command.tsx index 45755e88a..ce5a2c5e4 100644 --- a/apps/discord-bot/src/commands/ratios/ratios.command.tsx +++ b/apps/discord-bot/src/commands/ratios/ratios.command.tsx @@ -6,7 +6,6 @@ * https://github.com/Statsify/statsify/blob/main/LICENSE */ -import * as Sentry from "@sentry/node"; import { ARCADE_MODES, ARENA_BRAWL_MODES, @@ -233,20 +232,7 @@ export class RatiosCommand { }; const canvas = render(, getTheme(user)); - const transaction = Sentry.getCurrentHub().getScope()?.getTransaction(); - const span = transaction?.startChild({ - op: "canvas.encode_png", - description: "Encode ratios canvas as PNG", - }); - - let buffer: Buffer; - - try { - buffer = await canvas.toBuffer("png"); - span?.setData("png.bytes", buffer.byteLength); - } finally { - span?.finish(); - } + const buffer = await canvas.toBuffer("png"); return { files: [{ name: "ratios.png", data: buffer, type: "image/png" }], diff --git a/apps/discord-bot/src/lib/command.listener.ts b/apps/discord-bot/src/lib/command.listener.ts index 43491a9d9..c4dc6f20c 100644 --- a/apps/discord-bot/src/lib/command.listener.ts +++ b/apps/discord-bot/src/lib/command.listener.ts @@ -65,32 +65,33 @@ export class CommandListener extends AbstractCommandListener { parentData ); - const [name, ...subcommandParts] = commandName.split(" "); - const subcommand = subcommandParts.length ? commandName : undefined; - - const transaction = Sentry.startTransaction({ - name: commandName, - op: "discord.command.total", - data: { - "command.name": name, - "command.group": parentCommand.group ?? command.group ?? "unknown", - "command.subcommand": subcommand, - }, - tags: { - "command.name": name, - "command.group": parentCommand.group ?? command.group ?? "unknown", - "command.subcommand": subcommand ?? "none", - }, - }); + const [name, ...subcommandParts] = commandName.split(" "); + const group = parentCommand.group ?? command.group ?? "unknown"; + const subcommand = subcommandParts.join(" ") || undefined; + + const transaction = Sentry.startTransaction({ + name: commandName, + op: "command.total", + data: { + "command.name": name, + "command.group": group, + "command.subcommand": subcommand, + }, + tags: { + "command.name": name, + "command.group": group, + "command.subcommand": subcommand ?? "none", + }, + }); Sentry.configureScope((scope) => scope.setSpan(transaction)); - Sentry.setContext("command", { - command: commandName, - group: parentCommand.group ?? command.group ?? null, - name, - subcommand: subcommand ?? null, - options: data.options, + Sentry.setContext("command", { + command: commandName, + group, + name, + subcommand: subcommand ?? null, + options: data.options, guild: interaction.getGuildId() ?? null, }); diff --git a/assets/private b/assets/private index 7bd3861f9..38a97e46e 160000 --- a/assets/private +++ b/assets/private @@ -1 +1 @@ -Subproject commit 7bd3861f93050e270dff5666aac33dd4081aa22f +Subproject commit 38a97e46ee32b939b0f617bfd66a8833c7c6e619 diff --git a/packages/api-client/package.json b/packages/api-client/package.json index d911fa1fb..0cd661b49 100644 --- a/packages/api-client/package.json +++ b/packages/api-client/package.json @@ -14,6 +14,7 @@ "@nestjs/common": "^11.1.6", "@nestjs/swagger": "^11.2.0", "@sentry/node": "^7.118.0", + "@statsify/logger": "workspace:^", "@statsify/rendering": "workspace:^", "@statsify/schemas": "workspace:^", "@statsify/util": "workspace:^", diff --git a/packages/api-client/src/api.service.ts b/packages/api-client/src/api.service.ts index 126f1a644..71f239551 100644 --- a/packages/api-client/src/api.service.ts +++ b/packages/api-client/src/api.service.ts @@ -6,7 +6,6 @@ * https://github.com/Statsify/statsify/blob/main/LICENSE */ -import * as Sentry from "@sentry/node"; import Axios, { AxiosInstance, AxiosRequestHeaders, @@ -40,6 +39,7 @@ import { import { User, UserFooter, UserTheme } from "@statsify/schemas"; import { config } from "@statsify/util"; import { loadImage } from "@statsify/rendering"; +import { withSentrySpan } from "@statsify/logger"; interface ExtraData { headers?: AxiosRequestHeaders; @@ -294,21 +294,15 @@ export class ApiService { method: Method = "GET", { body, headers, responseType }: ExtraData = {} ): Promise { - const transaction = Sentry.getCurrentHub().getScope()?.getTransaction(); - - const child = transaction?.startChild({ - op: "statsify.api.fetch", + const res = await withSentrySpan({ + op: "statsify.fetch", description: `${method} ${url}`, data: { "http.method": method, "http.route": url, }, - }); - - let res: AxiosResponse; - - try { - res = await this.axios.request({ + }, async (span) => { + const response: AxiosResponse = await this.axios.request({ url, method, params, @@ -317,10 +311,10 @@ export class ApiService { responseType, }); - child?.setHttpStatus(res.status); - } finally { - child?.finish(); - } + span?.setHttpStatus(response.status); + + return response; + }); const data = res.data; diff --git a/packages/discord/src/command/abstract-command.listener.ts b/packages/discord/src/command/abstract-command.listener.ts index fce60d26e..c9e4e47ee 100644 --- a/packages/discord/src/command/abstract-command.listener.ts +++ b/packages/discord/src/command/abstract-command.listener.ts @@ -6,7 +6,6 @@ * https://github.com/Statsify/statsify/blob/main/LICENSE */ -import * as Sentry from "@sentry/node"; import { type APIUser, ApplicationCommandOptionType, @@ -17,7 +16,12 @@ import { CommandContext } from "./command.context.js"; import { ErrorMessage } from "#util/error.message"; import { type IMessage, Message } from "#messages"; import { Interaction, type InteractionAttachment } from "#interaction"; -import { Logger } from "@statsify/logger"; +import { + Logger, + getSentryTransaction, + setSentryMemoryUsage, + withSentrySpan, +} from "@statsify/logger"; import { User, UserTier } from "@statsify/schemas"; import { getAssetPath, getLogoPath } from "@statsify/assets"; import { readFileSync } from "node:fs"; @@ -144,7 +148,7 @@ export abstract class AbstractCommandListener { commandSpan?.finish(); if (typeof response !== "object") { - this.setMemoryUsage(transaction); + setSentryMemoryUsage(transaction); transaction?.finish(); return; } @@ -154,21 +158,19 @@ export abstract class AbstractCommandListener { ...response, }); - this.setMemoryUsage(transaction); + setSentryMemoryUsage(transaction); transaction?.finish(); } catch (err) { if (err instanceof Message) { - commandSpan?.finish(); await context.reply(err); - this.setMemoryUsage(transaction); + setSentryMemoryUsage(transaction); transaction?.finish(); return; } this.logger.error(`An error occurred when running "${commandName}"`); this.logger.error(err); - commandSpan?.finish(); - this.setMemoryUsage(transaction); + setSentryMemoryUsage(transaction); transaction?.finish(); } } @@ -335,14 +337,5 @@ export abstract class AbstractCommandListener { }); } - private setMemoryUsage(transaction?: Sentry.Transaction) { - if (!transaction) return; - - const { rss, heapUsed } = process.memoryUsage(); - - transaction.setData("memory.rss.bytes", rss); - transaction.setData("memory.heap_used.bytes", heapUsed); - } - protected abstract onCommand(interaction: Interaction): Promise | void; } diff --git a/packages/discord/src/interaction/interaction.ts b/packages/discord/src/interaction/interaction.ts index 5aadf84c5..6f5a84dcc 100644 --- a/packages/discord/src/interaction/interaction.ts +++ b/packages/discord/src/interaction/interaction.ts @@ -6,7 +6,6 @@ * https://github.com/Statsify/statsify/blob/main/LICENSE */ -import * as Sentry from "@sentry/node"; import { type APIGuildMember, type APIUser, @@ -15,6 +14,7 @@ import { } from "discord-api-types/v10"; import { type IMessage, Message, getLocalizeFunction } from "#messages"; import { parseDiscordResponse } from "#util/parse-discord-error"; +import { withSentrySpan } from "@statsify/logger"; import type { InteractionServer, RestClient, @@ -155,17 +155,31 @@ export class Interaction { } private async request(options: RestClient.RequestOptions) { - const transaction = Sentry.getCurrentHub().getScope()?.getTransaction(); - const span = transaction?.startChild({ - op: "discord.reply", - description: `${options.method.toUpperCase()} ${options.path}`, - }); + const route = this.getRouteName(options.path); - try { + return withSentrySpan({ + op: "discord.reply", + description: `${options.method.toUpperCase()} ${route}`, + data: { + "http.method": options.method.toUpperCase(), + "http.route": route, + }, + }, async () => { const response = await this.rest.request(options); return parseDiscordResponse(response); - } finally { - span?.finish(); - } + }); + } + + private getRouteName(path: string) { + return path + .replace( + /^\/interactions\/[^/]+\/[^/]+\/callback$/, + "/interactions/:interactionId/:interactionToken/callback" + ) + .replace( + /^\/webhooks\/[^/]+\/[^/]+/, + "/webhooks/:applicationId/:interactionToken" + ) + .replace(/\/messages\/[^/]+$/, "/messages/:messageId"); } } diff --git a/packages/discord/src/services/paginate.service.ts b/packages/discord/src/services/paginate.service.ts index e7aae18b9..ac7295d12 100644 --- a/packages/discord/src/services/paginate.service.ts +++ b/packages/discord/src/services/paginate.service.ts @@ -6,7 +6,6 @@ * https://github.com/Statsify/statsify/blob/main/LICENSE */ -import * as Sentry from "@sentry/node"; import { ButtonStyle } from "discord-api-types/v10"; import { Canvas } from "skia-canvas"; diff --git a/packages/logger/src/index.ts b/packages/logger/src/index.ts index d2f715e65..cd22d44d9 100644 --- a/packages/logger/src/index.ts +++ b/packages/logger/src/index.ts @@ -14,6 +14,15 @@ import type { ConsoleLoggerOptions, LogLevel, LoggerService } from "@nestjs/comm const DEFAULT_LOG_LEVELS: LogLevel[] = ["log", "error", "warn", "debug", "verbose", "fatal"]; +type SentryTagValue = boolean | number | string | null | undefined; + +export interface SentrySpanOptions { + op: string; + description?: string; + data?: Record; + tags?: Record; +} + export const STATUS_COLORS = { debug: 0xC700E7, warn: 0xFAB627, @@ -34,6 +43,61 @@ const ColorByLogLevel: Record = { const isProduction = await config("environment") === "prod"; +export function getSentryTransaction() { + return Sentry.getCurrentHub().getScope()?.getTransaction(); +} + +export function startSentrySpan({ + op, + description, + data, + tags, +}: SentrySpanOptions) { + const span = getSentryTransaction()?.startChild({ op, description, data }); + + for (const [key, value] of Object.entries(tags ?? {})) { + if (value === null || value === undefined) continue; + span?.setTag(key, String(value)); + } + + return span; +} + +export async function withSentrySpan( + options: SentrySpanOptions, + callback: (span?: Sentry.Span) => Promise +): Promise { + const span = startSentrySpan(options); + + try { + return await callback(span); + } finally { + span?.finish(); + } +} + +export function withSentrySpanSync( + options: SentrySpanOptions, + callback: (span?: Sentry.Span) => T +): T { + const span = startSentrySpan(options); + + try { + return callback(span); + } finally { + span?.finish(); + } +} + +export function setSentryMemoryUsage(span = getSentryTransaction()) { + if (!span) return; + + const { rss, heapUsed } = process.memoryUsage(); + + span.setData("memory.rss.bytes", rss); + span.setData("memory.heap_used.bytes", heapUsed); +} + /** * A logger implementing the NestJS LoggerService interface. However can be used anywhere. * Outputs: {icon} {context} {time} {message} diff --git a/packages/rendering/package.json b/packages/rendering/package.json index 1a56658c8..737958049 100644 --- a/packages/rendering/package.json +++ b/packages/rendering/package.json @@ -22,6 +22,7 @@ "dependencies": { "@sentry/node": "^7.118.0", "@statsify/assets": "workspace:^", + "@statsify/logger": "workspace:^", "@statsify/util": "workspace:^", "@swc/helpers": "^0.5.23", "axios": "1.11.0", diff --git a/packages/rendering/src/canvas.ts b/packages/rendering/src/canvas.ts index 609cc0b68..05c3e61c3 100644 --- a/packages/rendering/src/canvas.ts +++ b/packages/rendering/src/canvas.ts @@ -7,8 +7,61 @@ */ import { Canvas } from "skia-canvas"; +import { startSentrySpan } from "@statsify/logger"; type CanvasOptions = ConstructorParameters[2] & { gpu?: boolean }; +type CanvasToBuffer = typeof Canvas.prototype.toBuffer; + +let canvasToBufferInstrumented = false; + +function instrumentCanvasToBuffer() { + if (canvasToBufferInstrumented) return; + canvasToBufferInstrumented = true; + + const toBuffer = Canvas.prototype.toBuffer; + + Canvas.prototype.toBuffer = function instrumentedToBuffer( + this: Canvas, + ...args: Parameters + ): ReturnType { + const format = args[0]; + const isPng = format === undefined || format === "png"; + + if (!isPng) return toBuffer.apply(this, args) as ReturnType; + + const span = startSentrySpan({ + op: "png.encode", + description: "Encode canvas as PNG", + data: { + width: this.width, + height: this.height, + }, + }); + + let result: ReturnType; + + try { + result = toBuffer.apply(this, args) as ReturnType; + } catch (error) { + span?.finish(); + throw error; + } + + if (!result || typeof (result as Promise).then !== "function") { + span?.finish(); + return result; + } + + return (result as Promise) + .then((buffer) => { + span?.setData("png.bytes", buffer.byteLength); + return buffer; + }) + .finally(() => span?.finish()) as ReturnType; + }; +} + +instrumentCanvasToBuffer(); export function createCanvas( width?: number, diff --git a/packages/rendering/src/jsx/render.ts b/packages/rendering/src/jsx/render.ts index 4eba53de4..fedf7ef05 100644 --- a/packages/rendering/src/jsx/render.ts +++ b/packages/rendering/src/jsx/render.ts @@ -15,6 +15,7 @@ import { createCanvas } from "../canvas.js"; import { createInstructions } from "./create-instructions.js"; import { getPositionalDelta, getTotalSize } from "./util.js"; import { noop } from "@statsify/util"; +import { withSentrySpanSync } from "@statsify/logger"; import type { ComputedThemeContext, ElementNode, @@ -119,49 +120,41 @@ const renderRecursive = ( }; export function render(node: ElementNode, theme?: Theme): Canvas { - const transaction = Sentry.getCurrentHub().getScope()?.getTransaction(); - - const instructionsTransaction = transaction?.startChild({ - op: "render.create_instructions", + const instructions = withSentrySpanSync({ + op: "render.instructions", description: "Create instructions", - }); - - const instructions = createInstructions(node); - - instructionsTransaction?.finish(); + }, () => createInstructions(node)); const width = Math.round(getTotalSize(instructions.x)); const height = Math.round(getTotalSize(instructions.y)); - const renderTransaction = transaction?.startChild({ + return withSentrySpanSync({ op: "render.generate", description: "Generate render canvas", data: { width, height }, + }, () => { + const canvas = createCanvas(width, height); + const ctx = canvas.getContext("2d"); + ctx.imageSmoothingEnabled = false; + + const context: ComputedThemeContext = { + renderer: noop(), + ...theme?.context, + canvasWidth: width, + canvasHeight: height, + }; + + if (!context.renderer) context.renderer = Container.get(FontRenderer); + + renderRecursive( + ctx, + context, + { ...intrinsicRenders, ...theme?.elements }, + instructions, + 0, + 0 + ); + + return canvas; }); - - const canvas = createCanvas(width, height); - const ctx = canvas.getContext("2d"); - ctx.imageSmoothingEnabled = false; - - const context: ComputedThemeContext = { - renderer: noop(), - ...theme?.context, - canvasWidth: width, - canvasHeight: height, - }; - - if (!context.renderer) context.renderer = Container.get(FontRenderer); - - renderRecursive( - ctx, - context, - { ...intrinsicRenders, ...theme?.elements }, - instructions, - 0, - 0 - ); - - renderTransaction?.finish(); - - return canvas; } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 50476687e..e0ee130c6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -475,6 +475,9 @@ importers: '@sentry/node': specifier: ^7.118.0 version: 7.120.4 + '@statsify/logger': + specifier: workspace:^ + version: link:../logger '@statsify/rendering': specifier: workspace:^ version: link:../rendering @@ -603,6 +606,9 @@ importers: '@statsify/assets': specifier: workspace:^ version: link:../assets + '@statsify/logger': + specifier: workspace:^ + version: link:../logger '@statsify/util': specifier: workspace:^ version: link:../util From 75a9d71b69da3f85c5a88481e1f8f4b355a4939f Mon Sep 17 00:00:00 2001 From: Cody Date: Sat, 30 May 2026 15:42:09 -0600 Subject: [PATCH 03/11] chore(.gitmodules): ensure consistent formatting for submodule URLs --- .gitmodules | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitmodules b/.gitmodules index 5e3e9437a..43b90d09d 100644 --- a/.gitmodules +++ b/.gitmodules @@ -3,4 +3,4 @@ url = https://github.com/Statsify/public-assets [submodule "assets/private"] path = assets/private - url = https://github.com/Statsify/assets \ No newline at end of file + url = https://github.com/Statsify/assets From 513d0fa7bfc1cbebbe92a2566cb61964cced47fb Mon Sep 17 00:00:00 2001 From: Cody Date: Sat, 30 May 2026 15:44:57 -0600 Subject: [PATCH 04/11] chore: add sentry profile sample config --- config.schema.js | 1 + 1 file changed, 1 insertion(+) diff --git a/config.schema.js b/config.schema.js index 4317d8ae2..a2787face 100644 --- a/config.schema.js +++ b/config.schema.js @@ -68,6 +68,7 @@ export default { verifyServerDsn: "", supportBotDsn: "", tracesSampleRate: 1, + profilesSampleRate: 1, }, environment: "dev", }; From 841a6a04a38e57714320d3247f6efd910a12ab82 Mon Sep 17 00:00:00 2001 From: Cody Date: Sat, 30 May 2026 20:43:45 -0600 Subject: [PATCH 05/11] feat: attach hypixel ratelimit headers to spans --- apps/api/src/hypixel/hypixel.service.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/apps/api/src/hypixel/hypixel.service.ts b/apps/api/src/hypixel/hypixel.service.ts index 84aec794d..5e2849dfa 100644 --- a/apps/api/src/hypixel/hypixel.service.ts +++ b/apps/api/src/hypixel/hypixel.service.ts @@ -153,6 +153,9 @@ export class HypixelService { return this.httpService.get(url, { params }).pipe( tap((res) => { span?.setHttpStatus(res.status); + span?.setData("hypixel.ratelimit.limit", res.headers["ratelimit-limit"]); + span?.setData("hypixel.ratelimit.remaining", res.headers["ratelimit-remaining"]); + span?.setData("hypixel.ratelimit.reset", res.headers["ratelimit-reset"]); }), map((res) => res.data), catchError((err) => throwError( From b6edcc2009deb95ff08ed3338d774cbcc54b7358 Mon Sep 17 00:00:00 2001 From: Cody Date: Tue, 2 Jun 2026 20:52:22 -0600 Subject: [PATCH 06/11] feat(hypixel, leaderboard, command, api, logger, rendering): enhance observability with additional Sentry attributes * Added endpoint and HTTP method data to HypixelService * Included leaderboard type and fields in LeaderboardService * Captured guild ID and user tier in CommandListener * Implemented cache hit tracking in ApiService * Enhanced logging with structured attributes in Logger * Updated rendering functions to include width and height in Sentry data --- apps/api/src/hypixel/hypixel.service.ts | 1 + .../src/leaderboards/leaderboard.service.ts | 22 ++++++-- apps/discord-bot/src/lib/command.listener.ts | 54 ++++++++++--------- packages/api-client/src/api.service.ts | 21 ++++++++ .../src/command/abstract-command.listener.ts | 7 +++ packages/logger/src/index.ts | 40 ++++++++++++++ packages/rendering/src/canvas.ts | 4 +- packages/rendering/src/jsx/render.ts | 5 +- 8 files changed, 124 insertions(+), 30 deletions(-) diff --git a/apps/api/src/hypixel/hypixel.service.ts b/apps/api/src/hypixel/hypixel.service.ts index 5e2849dfa..5b017e20a 100644 --- a/apps/api/src/hypixel/hypixel.service.ts +++ b/apps/api/src/hypixel/hypixel.service.ts @@ -145,6 +145,7 @@ export class HypixelService { op: "hypixel.fetch", description: `GET ${this.httpService.axiosRef.getUri({ url })}`, data: { + "hypixel.endpoint": url, "http.method": "GET", "http.route": url, }, diff --git a/apps/api/src/leaderboards/leaderboard.service.ts b/apps/api/src/leaderboards/leaderboard.service.ts index d3327da08..e5106d0b8 100644 --- a/apps/api/src/leaderboards/leaderboard.service.ts +++ b/apps/api/src/leaderboards/leaderboard.service.ts @@ -70,6 +70,9 @@ export abstract class LeaderboardService { await withSentrySpan({ op: "redis.write", description: `add ${constructor.name} leaderboards`, + data: { + "leaderboard.type": name, + }, }, () => pipeline.exec()); } @@ -125,7 +128,8 @@ export abstract class LeaderboardService { field, top, bottom - 1, - sort + sort, + type ); const additionalFieldMetadata = additionalFields.map((k) => @@ -218,6 +222,10 @@ export abstract class LeaderboardService { const responses = await withSentrySpan({ op: "redis.get", description: `get ${constructor.name} rankings`, + data: { + "leaderboard.fields": fields, + "leaderboard.type": constructorName, + }, }, () => pipeline.exec()); if (!responses) throw new InternalServerErrorException(); @@ -267,14 +275,22 @@ export abstract class LeaderboardService { field: string, top: number, bottom: number, - sort = "DESC" + sort = "DESC", + queryType?: LeaderboardQuery ) { const name = constructor.name.toLowerCase(); - field = `${name}.${field}`; + const leaderboardField = field; + field = `${name}.${leaderboardField}`; const scores = await withSentrySpan({ op: "redis.get", description: `get ${constructor.name} leaderboards`, + data: { + "leaderboard.field": leaderboardField, + "leaderboard.query_type": queryType, + "leaderboard.sort": sort, + "leaderboard.type": name, + }, }, () => sort === "ASC" ? this.redis.zrange(field, top, bottom, "WITHSCORES") : diff --git a/apps/discord-bot/src/lib/command.listener.ts b/apps/discord-bot/src/lib/command.listener.ts index c4dc6f20c..28da5ee4c 100644 --- a/apps/discord-bot/src/lib/command.listener.ts +++ b/apps/discord-bot/src/lib/command.listener.ts @@ -65,33 +65,39 @@ export class CommandListener extends AbstractCommandListener { parentData ); - const [name, ...subcommandParts] = commandName.split(" "); - const group = parentCommand.group ?? command.group ?? "unknown"; - const subcommand = subcommandParts.join(" ") || undefined; - - const transaction = Sentry.startTransaction({ - name: commandName, - op: "command.total", - data: { - "command.name": name, - "command.group": group, - "command.subcommand": subcommand, - }, - tags: { - "command.name": name, - "command.group": group, - "command.subcommand": subcommand ?? "none", - }, - }); + const [name, ...subcommandParts] = commandName.split(" "); + const group = parentCommand.group ?? command.group ?? "unknown"; + const subcommand = subcommandParts.join(" ") || undefined; + + const transaction = Sentry.startTransaction({ + name: commandName, + op: "command.total", + data: { + "command.name": name, + "command.group": group, + "command.subcommand": subcommand, + "guild.id": interaction.getGuildId(), + }, + tags: { + "command.name": name, + "command.group": group, + "command.subcommand": subcommand ?? "none", + }, + tags: { + "command.name": name, + "command.group": group, + "command.subcommand": subcommand ?? "none", + }, + }); Sentry.configureScope((scope) => scope.setSpan(transaction)); - Sentry.setContext("command", { - command: commandName, - group, - name, - subcommand: subcommand ?? null, - options: data.options, + Sentry.setContext("command", { + command: commandName, + group, + name, + subcommand: subcommand ?? null, + options: data.options, guild: interaction.getGuildId() ?? null, }); diff --git a/packages/api-client/src/api.service.ts b/packages/api-client/src/api.service.ts index 71f239551..263136d22 100644 --- a/packages/api-client/src/api.service.ts +++ b/packages/api-client/src/api.service.ts @@ -47,6 +47,23 @@ interface ExtraData { responseType?: ResponseType; } +function getCacheHit(data: unknown): boolean | undefined { + if (!data || typeof data !== "object") return undefined; + + if ("cached" in data && typeof data.cached === "boolean") { + return data.cached; + } + + for (const value of Object.values(data)) { + if (value && typeof value === "object" && "cached" in value) { + const cached = (value as { cached?: unknown }).cached; + if (typeof cached === "boolean") return cached; + } + } + + return undefined; +} + // TODO: Move dtos in api to @statsify/api-client interface UpdateUser { serverMember?: boolean; @@ -298,6 +315,7 @@ export class ApiService { op: "statsify.fetch", description: `${method} ${url}`, data: { + "cache.level": typeof params?.cache === "string" ? params.cache : undefined, "http.method": method, "http.route": url, }, @@ -313,6 +331,9 @@ export class ApiService { span?.setHttpStatus(response.status); + const cacheHit = getCacheHit(response.data); + if (cacheHit !== undefined) span.setAttribute("cache.hit", cacheHit); + return response; }); diff --git a/packages/discord/src/command/abstract-command.listener.ts b/packages/discord/src/command/abstract-command.listener.ts index c9e4e47ee..82e20a238 100644 --- a/packages/discord/src/command/abstract-command.listener.ts +++ b/packages/discord/src/command/abstract-command.listener.ts @@ -137,6 +137,13 @@ export abstract class AbstractCommandListener { const commandSpan = transaction?.startChild({ op: "discord.command.execute", description: commandName, + data: { + "command.name": command.name, + "command.group": command.group, + "command.full_name": commandName, + "guild.id": context.getInteraction().getGuildId(), + "user.tier": context.getUser()?.tier ?? UserTier.NONE, + }, }); try { diff --git a/packages/logger/src/index.ts b/packages/logger/src/index.ts index cd22d44d9..429cea828 100644 --- a/packages/logger/src/index.ts +++ b/packages/logger/src/index.ts @@ -16,6 +16,8 @@ const DEFAULT_LOG_LEVELS: LogLevel[] = ["log", "error", "warn", "debug", "verbos type SentryTagValue = boolean | number | string | null | undefined; +type SentryLogLevel = Extract; + export interface SentrySpanOptions { op: string; description?: string; @@ -98,6 +100,18 @@ export function setSentryMemoryUsage(span = getSentryTransaction()) { span.setData("memory.heap_used.bytes", heapUsed); } +function stringifyMessage(message: unknown) { + if (message instanceof Error) return message.message; + if (typeof message === "string") return message; + if (typeof message !== "object" || message === null) return String(message); + + try { + return JSON.stringify(message); + } catch { + return String(message); + } +} + /** * A logger implementing the NestJS LoggerService interface. However can be used anywhere. * Outputs: {icon} {context} {time} {message} @@ -157,6 +171,9 @@ export class Logger implements LoggerService { ...optionalParameters, ]); + const sentryLog = this.getContextAndMessages([message, ...optionalParameters]); + + Logger.captureSentryLog(sentryLog.messages, sentryLog.context, "error"); Logger.printMessage(messages, context, "error", "stderr", "📉"); } @@ -172,6 +189,7 @@ export class Logger implements LoggerService { ...optionalParameters, ]); + Logger.captureSentryLog(messages, context, "warn"); Logger.printMessage(messages, context, "warn"); } @@ -227,6 +245,9 @@ export class Logger implements LoggerService { ...optionalParameters, ]); + const sentryLog = this.getContextAndMessages([message, ...optionalParameters]); + + Logger.captureSentryLog(sentryLog.messages, sentryLog.context, "fatal"); Logger.printMessage(messages, context, "fatal", "stderr", "📉"); } @@ -300,6 +321,25 @@ export class Logger implements LoggerService { process[writeStreamType].write(computedMessage); } } + + private static captureSentryLog( + messages: unknown[], + context: string | undefined, + logLevel: SentryLogLevel + ) { + for (const message of messages) { + Sentry.logger[logLevel](stringifyMessage(message), { + "logger.context": context ?? "Default", + "logger.level": logLevel, + ...(message instanceof Error ? + { + "error.name": message.name, + "error.message": message.message, + } : + {}), + }); + } + } } diff --git a/packages/rendering/src/canvas.ts b/packages/rendering/src/canvas.ts index 05c3e61c3..6ae84f5ff 100644 --- a/packages/rendering/src/canvas.ts +++ b/packages/rendering/src/canvas.ts @@ -33,8 +33,8 @@ function instrumentCanvasToBuffer() { op: "png.encode", description: "Encode canvas as PNG", data: { - width: this.width, - height: this.height, + "render.width": this.width, + "render.height": this.height, }, }); diff --git a/packages/rendering/src/jsx/render.ts b/packages/rendering/src/jsx/render.ts index fedf7ef05..c525c916a 100644 --- a/packages/rendering/src/jsx/render.ts +++ b/packages/rendering/src/jsx/render.ts @@ -131,7 +131,10 @@ export function render(node: ElementNode, theme?: Theme): Canvas { return withSentrySpanSync({ op: "render.generate", description: "Generate render canvas", - data: { width, height }, + data: { + "render.width": width, + "render.height": height, + }, }, () => { const canvas = createCanvas(width, height); const ctx = canvas.getContext("2d"); From d49bf1271d7fed6aba3a7b31177645e2ea841e0b Mon Sep 17 00:00:00 2001 From: Cody Date: Tue, 2 Jun 2026 22:32:07 -0600 Subject: [PATCH 07/11] chore(pnpm): add '@sentry-internal/node-cpu-profiler' to onlyBuiltDependencies --- pnpm-workspace.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 795b137c5..c9d7ac012 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -5,6 +5,7 @@ packages: onlyBuiltDependencies: - '@nestjs/core' + - '@sentry-internal/node-cpu-profiler' - '@scarf/scarf' - '@swc/core' - esbuild From 4097924ed1cd263af3ead57d298159030706db40 Mon Sep 17 00:00:00 2001 From: Cody Date: Tue, 16 Jun 2026 02:32:04 -0600 Subject: [PATCH 08/11] feat(sentry): enhance observability with Sentry spans * Replace span finish calls with end for consistency * Update Sentry integration methods for better performance * Refactor command listener to use startInactiveSpan * Improve error handling and context setting in commands --- apps/api/src/hypixel/hypixel.service.ts | 11 +- apps/api/src/index.ts | 4 +- .../src/leaderboards/leaderboard.service.ts | 7 - apps/api/src/redis/redis.utils.ts | 59 +------- apps/api/src/sentry/mongoose.ts | 13 +- apps/api/src/sentry/sentry.interceptor.ts | 23 ++-- apps/discord-bot/src/index.ts | 13 +- apps/discord-bot/src/lib/command.listener.ts | 128 ++++++++---------- apps/support-bot/src/index.ts | 2 +- apps/support-bot/src/lib/command.listener.ts | 20 ++- apps/verify-server/src/index.ts | 2 +- packages/api-client/src/api.service.ts | 7 +- .../src/command/abstract-command.listener.ts | 40 +++--- packages/logger/src/index.ts | 91 +++++++++---- packages/rendering/src/canvas.ts | 8 +- packages/rendering/src/jsx/render.ts | 1 - 16 files changed, 204 insertions(+), 225 deletions(-) diff --git a/apps/api/src/hypixel/hypixel.service.ts b/apps/api/src/hypixel/hypixel.service.ts index 5b017e20a..dbc8bcec0 100644 --- a/apps/api/src/hypixel/hypixel.service.ts +++ b/apps/api/src/hypixel/hypixel.service.ts @@ -6,6 +6,7 @@ * https://github.com/Statsify/statsify/blob/main/LICENSE */ +import * as Sentry from "@sentry/node"; import { CacheLevel } from "@statsify/api-client"; import { GameCounts, @@ -153,10 +154,10 @@ export class HypixelService { return this.httpService.get(url, { params }).pipe( tap((res) => { - span?.setHttpStatus(res.status); - span?.setData("hypixel.ratelimit.limit", res.headers["ratelimit-limit"]); - span?.setData("hypixel.ratelimit.remaining", res.headers["ratelimit-remaining"]); - span?.setData("hypixel.ratelimit.reset", res.headers["ratelimit-reset"]); + if (span) Sentry.setHttpStatus(span, res.status); + span?.setAttribute("hypixel.ratelimit.limit", res.headers["ratelimit-limit"]); + span?.setAttribute("hypixel.ratelimit.remaining", res.headers["ratelimit-remaining"]); + span?.setAttribute("hypixel.ratelimit.reset", res.headers["ratelimit-reset"]); }), map((res) => res.data), catchError((err) => throwError( @@ -165,7 +166,7 @@ export class HypixelService { cause: err, }) )), - finalize(() => span?.finish()) + finalize(() => span?.end()) ); } } diff --git a/apps/api/src/index.ts b/apps/api/src/index.ts index 3e6694d19..acbbe9ad9 100644 --- a/apps/api/src/index.ts +++ b/apps/api/src/index.ts @@ -41,8 +41,8 @@ if (sentryDsn) { Sentry.init({ dsn: sentryDsn, integrations: [ - new Sentry.Integrations.Http({ tracing: false, breadcrumbs: true }), - new Sentry.Integrations.Mongo({ useMongoose: true }), + Sentry.httpIntegration({ spans: false, breadcrumbs: true }), + Sentry.mongoIntegration(), nodeProfilingIntegration(), ], normalizeDepth: 3, diff --git a/apps/api/src/leaderboards/leaderboard.service.ts b/apps/api/src/leaderboards/leaderboard.service.ts index e5106d0b8..9616df573 100644 --- a/apps/api/src/leaderboards/leaderboard.service.ts +++ b/apps/api/src/leaderboards/leaderboard.service.ts @@ -38,13 +38,6 @@ export abstract class LeaderboardService { remove = false ) { const fields = getLeaderboardFields(constructor); - const transaction = Sentry.getCurrentHub().getScope()?.getTransaction(); - - const child = transaction?.startChild({ - op: "redis", - description: `add ${constructor.name} leaderboards`, - }); - const pipeline = this.redis.pipeline(); const name = constructor.name.toLowerCase(); diff --git a/apps/api/src/redis/redis.utils.ts b/apps/api/src/redis/redis.utils.ts index 74490c286..8e57a607e 100644 --- a/apps/api/src/redis/redis.utils.ts +++ b/apps/api/src/redis/redis.utils.ts @@ -12,38 +12,8 @@ import { REDIS_MODULE_OPTIONS_TOKEN, } from "./redis.constants.js"; import { Redis } from "ioredis"; -import { startSentrySpan } from "@statsify/logger"; import type { RedisModuleOptions } from "./redis.interfaces.js"; -const REDIS_READ_COMMANDS = new Set([ - "exists", - "get", - "hget", - "hgetall", - "hmget", - "mget", - "ttl", - "zrank", - "zrange", - "zrevrank", - "zrevrange", - "zscore", - "ft.sugget", -]); - -const REDIS_WRITE_COMMANDS = new Set([ - "del", - "expire", - "expireat", - "hset", - "hmset", - "set", - "zadd", - "zrem", - "ft.sugadd", - "ft.sugdel", -]); - export function getRedisOptionsToken(connection?: string): string { return `${connection || REDIS_MODULE_CONNECTION}_${REDIS_MODULE_OPTIONS_TOKEN}`; } @@ -54,32 +24,5 @@ export function getRedisConnectionToken(connection?: string): string { export function createRedisConnection(options: RedisModuleOptions) { const { config } = options; - const redis = config.url ? new Redis(config.url, config) : new Redis(config); - const sendCommand = redis.sendCommand.bind(redis); - - redis.sendCommand = ((command, stream) => { - const commandName = String((command as { name: string }).name).toLowerCase(); - const span = startSentrySpan({ - op: getRedisSpanOperation(commandName), - description: commandName, - data: { "redis.command": commandName }, - }); - - try { - return (sendCommand(command, stream) as Promise).finally(() => - span?.finish() - ); - } catch (error) { - span?.finish(); - throw error; - } - }) as Redis["sendCommand"]; - - return redis; -} - -function getRedisSpanOperation(commandName: string) { - if (REDIS_READ_COMMANDS.has(commandName)) return "redis.get"; - if (REDIS_WRITE_COMMANDS.has(commandName)) return "redis.write"; - return "redis.command"; + return config.url ? new Redis(config.url, config) : new Redis(config); } diff --git a/apps/api/src/sentry/mongoose.ts b/apps/api/src/sentry/mongoose.ts index ccf6adae6..3c6a7baf3 100644 --- a/apps/api/src/sentry/mongoose.ts +++ b/apps/api/src/sentry/mongoose.ts @@ -52,11 +52,11 @@ function instrumentQueryExec() { }); try { - return exec.apply(this, args).finally(() => span?.finish()) as ReturnType< + return exec.apply(this, args).finally(() => span?.end()) as ReturnType< typeof exec >; } catch (error) { - span?.finish(); + span?.end(); throw error; } }; @@ -69,7 +69,8 @@ function instrumentAggregateExec() { this: InstrumentedAggregate, ...args: Parameters ): ReturnType { - const collection = this._model?.collection?.name ?? "unknown"; + const model = this["_model"]; + const collection = model?.collection?.name ?? "unknown"; const span = startSentrySpan({ op: "mongo.query", description: `${collection}.aggregate`, @@ -77,16 +78,16 @@ function instrumentAggregateExec() { "db.collection": collection, "db.operation": "aggregate", "db.system": "mongodb", - "mongoose.model": this._model?.modelName ?? "unknown", + "mongoose.model": model?.modelName ?? "unknown", }, }); try { - return exec.apply(this, args).finally(() => span?.finish()) as ReturnType< + return exec.apply(this, args).finally(() => span?.end()) as ReturnType< typeof exec >; } catch (error) { - span?.finish(); + span?.end(); throw error; } }; diff --git a/apps/api/src/sentry/sentry.interceptor.ts b/apps/api/src/sentry/sentry.interceptor.ts index 76c868696..7de5e03c1 100644 --- a/apps/api/src/sentry/sentry.interceptor.ts +++ b/apps/api/src/sentry/sentry.interceptor.ts @@ -36,34 +36,39 @@ export class SentryInterceptor implements NestInterceptor { headers: req.headers, }); - let transaction: ReturnType | undefined; + let transaction: Sentry.Span | undefined; if (!url.pathname.includes("/skin")) { - transaction = Sentry.startTransaction({ + transaction = Sentry.startInactiveSpan({ op: "request", name: `${req.method} ${url.pathname}`, + forceTransaction: true, }); - - Sentry.configureScope((scope) => scope.setSpan(transaction)); } - return next.handle().pipe( + const response$ = next.handle().pipe( catchError((err) => { const isHttpException = err instanceof HttpException; const isInternalError = err instanceof InternalServerErrorException; if (isHttpException && !isInternalError) { - transaction?.finish(); + transaction?.end(); throw err; } Sentry.captureException(err); - transaction?.setHttpStatus(500); - transaction?.finish(); + if (transaction) Sentry.setHttpStatus(transaction, 500); + transaction?.end(); throw err; }), - tap(() => transaction?.finish()) + tap(() => transaction?.end()) ); + + return transaction ? + new Observable((subscriber) => + Sentry.withActiveSpan(transaction, () => response$.subscribe(subscriber)) + ) : + response$; } } diff --git a/apps/discord-bot/src/index.ts b/apps/discord-bot/src/index.ts index b285cd91c..2500730be 100644 --- a/apps/discord-bot/src/index.ts +++ b/apps/discord-bot/src/index.ts @@ -15,8 +15,7 @@ import { InteractionServer, RestClient, WebsocketShard } from "tiny-discord"; import { Logger } from "@statsify/logger"; import { VerifyCommand } from "#commands/verify.command"; import { config } from "@statsify/util"; -import { dirname, join } from "node:path"; -import { fileURLToPath } from "node:url"; +import { join } from "node:path"; import { nodeProfilingIntegration } from "@sentry/profiling-node"; const directory = import.meta.dirname; @@ -36,11 +35,11 @@ const sentryProfilesSampleRate = if (sentryDsn) { Sentry.init({ - dsn: sentryDsn, - integrations: [ - new Sentry.Integrations.Http({ tracing: false, breadcrumbs: true }), - nodeProfilingIntegration(), - ], + dsn: sentryDsn, + integrations: [ + Sentry.httpIntegration({ spans: false, breadcrumbs: true }), + nodeProfilingIntegration(), + ], normalizeDepth: 3, tracesSampleRate: sentryTracesSampleRate, profilesSampleRate: sentryProfilesSampleRate, diff --git a/apps/discord-bot/src/lib/command.listener.ts b/apps/discord-bot/src/lib/command.listener.ts index 28da5ee4c..aeb0ac2c3 100644 --- a/apps/discord-bot/src/lib/command.listener.ts +++ b/apps/discord-bot/src/lib/command.listener.ts @@ -63,74 +63,66 @@ export class CommandListener extends AbstractCommandListener { const [command, data, commandName] = this.getCommandAndData( parentCommand, parentData - ); - - const [name, ...subcommandParts] = commandName.split(" "); - const group = parentCommand.group ?? command.group ?? "unknown"; - const subcommand = subcommandParts.join(" ") || undefined; - - const transaction = Sentry.startTransaction({ - name: commandName, - op: "command.total", - data: { - "command.name": name, - "command.group": group, - "command.subcommand": subcommand, - "guild.id": interaction.getGuildId(), - }, - tags: { - "command.name": name, - "command.group": group, - "command.subcommand": subcommand ?? "none", - }, - tags: { - "command.name": name, - "command.group": group, - "command.subcommand": subcommand ?? "none", - }, - }); - - Sentry.configureScope((scope) => scope.setSpan(transaction)); - - Sentry.setContext("command", { - command: commandName, - group, - name, - subcommand: subcommand ?? null, - options: data.options, - guild: interaction.getGuildId() ?? null, - }); - - const user = await this.apiService.getUser(id); - - Sentry.setUser({ - id, - username: `${username}#${discriminator}`, - locale, - uuid: user?.uuid ?? null, - tier: user?.tier ?? UserTier.NONE, - serverMember: user?.serverMember ?? false, - theme: user?.theme ?? null, - }); - - const context = new CommandContext(this, interaction, data); - context.setUser(user); - - const preconditions = [ - this.tierPrecondition.bind(this, command, user), - this.cooldownPrecondition.bind(this, parentCommand, user, id), - ]; - - this.apiService.incrementCommand(commandName); - - return this.executeCommand({ - commandName, - command, - context, - preconditions, - message: this.getTipResponse(commandName, user), - }); - } + ); + + const [name, ...subcommandParts] = commandName.split(" "); + const group = parentCommand.group ?? "unknown"; + const subcommand = subcommandParts.join(" ") || undefined; + + const transaction = Sentry.startInactiveSpan({ + name: commandName, + op: "command.total", + forceTransaction: true, + attributes: { + "command.name": name, + "command.group": group, + "command.subcommand": subcommand, + "guild.id": interaction.getGuildId(), + }, + }); + + return Sentry.withActiveSpan(transaction, async () => { + Sentry.setContext("command", { + command: commandName, + group, + name, + subcommand: subcommand ?? null, + options: data.options, + guild: interaction.getGuildId() ?? null, + }); + + const user = await this.apiService.getUser(id); + + Sentry.setUser({ + id, + username: `${username}#${discriminator}`, + locale, + uuid: user?.uuid ?? null, + tier: user?.tier ?? UserTier.NONE, + serverMember: user?.serverMember ?? false, + theme: user?.theme ?? null, + }); + + const context = new CommandContext(this, interaction, data); + context.setUser(user); + + const preconditions = [ + this.tierPrecondition.bind(this, command, user), + this.cooldownPrecondition.bind(this, parentCommand, user, id), + ]; + + this.apiService.incrementCommand(commandName); + + return this.executeCommand({ + commandName, + command, + context, + observabilityGroup: group, + preconditions, + message: this.getTipResponse(commandName, user), + }); + }); + } private cooldownPrecondition( command: CommandResolvable, diff --git a/apps/support-bot/src/index.ts b/apps/support-bot/src/index.ts index 59a2f43e9..ad1196cfb 100644 --- a/apps/support-bot/src/index.ts +++ b/apps/support-bot/src/index.ts @@ -44,7 +44,7 @@ const sentryDsn = await config("sentry.supportBotDsn", { required: false }); if (sentryDsn) { Sentry.init({ dsn: sentryDsn, - integrations: [new Sentry.Integrations.Http({ tracing: false, breadcrumbs: true })], + integrations: [Sentry.httpIntegration({ spans: false, breadcrumbs: true })], normalizeDepth: 3, tracesSampleRate: await config("sentry.tracesSampleRate"), environment: await config("environment"), diff --git a/apps/support-bot/src/lib/command.listener.ts b/apps/support-bot/src/lib/command.listener.ts index e1445a37f..170b5632a 100644 --- a/apps/support-bot/src/lib/command.listener.ts +++ b/apps/support-bot/src/lib/command.listener.ts @@ -59,13 +59,19 @@ export class CommandListener extends AbstractCommandListener { const user = await this.apiService.getUser(id); - const context = new CommandContext(this, interaction, data); - context.setUser(user); - - const preconditions = [this.tierPrecondition.bind(this, command, user)]; - - return this.executeCommand({ commandName, command, context, preconditions }); - } + const context = new CommandContext(this, interaction, data); + context.setUser(user); + + const preconditions = [this.tierPrecondition.bind(this, command, user)]; + + return this.executeCommand({ + commandName, + command, + context, + observabilityGroup: parentCommand.group ?? "unknown", + preconditions, + }); + } public static create( client: WebsocketShard, diff --git a/apps/verify-server/src/index.ts b/apps/verify-server/src/index.ts index 0ce9286a3..c9134a808 100644 --- a/apps/verify-server/src/index.ts +++ b/apps/verify-server/src/index.ts @@ -40,7 +40,7 @@ const sentryDsn = await config("sentry.verifyServerDsn", { required: false }); if (sentryDsn) { Sentry.init({ dsn: sentryDsn, - integrations: [new Sentry.Integrations.Mongo({ useMongoose: true })], + integrations: [Sentry.mongoIntegration()], normalizeDepth: 3, tracesSampleRate: await config("sentry.tracesSampleRate"), environment: await config("environment"), diff --git a/packages/api-client/src/api.service.ts b/packages/api-client/src/api.service.ts index 263136d22..52f72bca6 100644 --- a/packages/api-client/src/api.service.ts +++ b/packages/api-client/src/api.service.ts @@ -6,7 +6,8 @@ * https://github.com/Statsify/statsify/blob/main/LICENSE */ -import Axios, { +import * as Sentry from "@sentry/node"; +import axios, { AxiosInstance, AxiosRequestHeaders, AxiosResponse, @@ -329,10 +330,10 @@ export class ApiService { responseType, }); - span?.setHttpStatus(response.status); + if (span) Sentry.setHttpStatus(span, response.status); const cacheHit = getCacheHit(response.data); - if (cacheHit !== undefined) span.setAttribute("cache.hit", cacheHit); + if (cacheHit !== undefined) span?.setAttribute("cache.hit", cacheHit); return response; }); diff --git a/packages/discord/src/command/abstract-command.listener.ts b/packages/discord/src/command/abstract-command.listener.ts index 82e20a238..6562b424d 100644 --- a/packages/discord/src/command/abstract-command.listener.ts +++ b/packages/discord/src/command/abstract-command.listener.ts @@ -20,7 +20,7 @@ import { Logger, getSentryTransaction, setSentryMemoryUsage, - withSentrySpan, + startSentrySpan, } from "@statsify/logger"; import { User, UserTier } from "@statsify/schemas"; import { getAssetPath, getLogoPath } from "@statsify/assets"; @@ -42,6 +42,7 @@ export interface ExecuteCommandOptions { commandName: string; command: CommandResolvable; context: CommandContext; + observabilityGroup?: string; preconditions?: CommandPrecondition[]; message?: IMessage | Message; } @@ -130,20 +131,21 @@ export abstract class AbstractCommandListener { commandName, command, context, + observabilityGroup, preconditions = [], message, }: ExecuteCommandOptions) { - const transaction = Sentry.getCurrentHub().getScope()?.getTransaction(); - const commandSpan = transaction?.startChild({ + const transaction = getSentryTransaction(); + const commandSpan = startSentrySpan({ op: "discord.command.execute", description: commandName, - data: { - "command.name": command.name, - "command.group": command.group, - "command.full_name": commandName, - "guild.id": context.getInteraction().getGuildId(), - "user.tier": context.getUser()?.tier ?? UserTier.NONE, - }, + data: { + "command.name": command.name, + "command.group": observabilityGroup ?? command.group ?? "unknown", + "command.full_name": commandName, + "guild.id": context.getInteraction().getGuildId(), + "user.tier": context.getUser()?.tier ?? UserTier.NONE, + }, }); try { @@ -152,11 +154,11 @@ export abstract class AbstractCommandListener { } const response = await command.execute(context); - commandSpan?.finish(); + commandSpan?.end(); if (typeof response !== "object") { setSentryMemoryUsage(transaction); - transaction?.finish(); + transaction?.end(); return; } @@ -166,19 +168,23 @@ export abstract class AbstractCommandListener { }); setSentryMemoryUsage(transaction); - transaction?.finish(); + transaction?.end(); } catch (err) { if (err instanceof Message) { - await context.reply(err); - setSentryMemoryUsage(transaction); - transaction?.finish(); + try { + await context.reply(err); + } finally { + setSentryMemoryUsage(transaction); + transaction?.end(); + } + return; } this.logger.error(`An error occurred when running "${commandName}"`); this.logger.error(err); setSentryMemoryUsage(transaction); - transaction?.finish(); + transaction?.end(); } } diff --git a/packages/logger/src/index.ts b/packages/logger/src/index.ts index 429cea828..03f049e9d 100644 --- a/packages/logger/src/index.ts +++ b/packages/logger/src/index.ts @@ -17,6 +17,13 @@ const DEFAULT_LOG_LEVELS: LogLevel[] = ["log", "error", "warn", "debug", "verbos type SentryTagValue = boolean | number | string | null | undefined; type SentryLogLevel = Extract; +type SentrySpanAttributeValue = + | boolean + | number + | string + | Array + | Array + | Array; export interface SentrySpanOptions { op: string; @@ -46,49 +53,75 @@ const ColorByLogLevel: Record = { const isProduction = await config("environment") === "prod"; export function getSentryTransaction() { - return Sentry.getCurrentHub().getScope()?.getTransaction(); + const activeSpan = Sentry.getActiveSpan(); + return activeSpan ? Sentry.getRootSpan(activeSpan) : undefined; } -export function startSentrySpan({ - op, - description, - data, - tags, -}: SentrySpanOptions) { - const span = getSentryTransaction()?.startChild({ op, description, data }); +function getSentrySpanAttribute(value: unknown): SentrySpanAttributeValue | undefined { + if (value === null || value === undefined) return undefined; + if (typeof value === "boolean" || typeof value === "number" || typeof value === "string") + return value; + + if (Array.isArray(value)) { + return value.map((entry) => entry === null || entry === undefined ? entry : String(entry)); + } + + try { + return JSON.stringify(value); + } catch { + return String(value); + } +} + +function getSentrySpanAttributes({ data, tags }: SentrySpanOptions) { + const attributes: Record = {}; + + for (const [key, value] of Object.entries(data ?? {})) { + attributes[key] = getSentrySpanAttribute(value); + } for (const [key, value] of Object.entries(tags ?? {})) { - if (value === null || value === undefined) continue; - span?.setTag(key, String(value)); + attributes[key] = getSentrySpanAttribute(value); } - return span; + return attributes; +} + +function getSentrySpanOptions(options: SentrySpanOptions) { + return { + name: options.description ?? options.op, + op: options.op, + attributes: getSentrySpanAttributes(options), + }; +} + +export function startSentrySpan(options: SentrySpanOptions) { + const parentSpan = getSentryTransaction(); + + if (!parentSpan) return undefined; + + return Sentry.startInactiveSpan({ + ...getSentrySpanOptions(options), + parentSpan, + }); } export async function withSentrySpan( options: SentrySpanOptions, callback: (span?: Sentry.Span) => Promise ): Promise { - const span = startSentrySpan(options); + if (!getSentryTransaction()) return callback(undefined); - try { - return await callback(span); - } finally { - span?.finish(); - } + return Sentry.startSpan(getSentrySpanOptions(options), callback); } export function withSentrySpanSync( options: SentrySpanOptions, callback: (span?: Sentry.Span) => T ): T { - const span = startSentrySpan(options); + if (!getSentryTransaction()) return callback(undefined); - try { - return callback(span); - } finally { - span?.finish(); - } + return Sentry.startSpan(getSentrySpanOptions(options), callback); } export function setSentryMemoryUsage(span = getSentryTransaction()) { @@ -96,8 +129,8 @@ export function setSentryMemoryUsage(span = getSentryTransaction()) { const { rss, heapUsed } = process.memoryUsage(); - span.setData("memory.rss.bytes", rss); - span.setData("memory.heap_used.bytes", heapUsed); + span.setAttribute("memory.rss.bytes", rss); + span.setAttribute("memory.heap_used.bytes", heapUsed); } function stringifyMessage(message: unknown) { @@ -159,8 +192,7 @@ export class Logger implements LoggerService { let normalizedMessage = message; if (message instanceof Error) { - const transaction = Sentry.getCurrentHub().getScope()?.getTransaction(); - transaction?.setStatus("internal_error"); + getSentryTransaction()?.setStatus({ code: 2, message: "internal_error" }); Sentry.captureException(message); normalizedMessage = message.stack; @@ -233,8 +265,7 @@ export class Logger implements LoggerService { let normalizedMessage = message; if (message instanceof Error) { - const transaction = Sentry.getCurrentHub().getScope()?.getTransaction(); - transaction?.setStatus("internal_error"); + getSentryTransaction()?.setStatus({ code: 2, message: "internal_error" }); Sentry.captureException(message); normalizedMessage = message.stack; @@ -327,6 +358,8 @@ export class Logger implements LoggerService { context: string | undefined, logLevel: SentryLogLevel ) { + if (!isProduction) return; + for (const message of messages) { Sentry.logger[logLevel](stringifyMessage(message), { "logger.context": context ?? "Default", diff --git a/packages/rendering/src/canvas.ts b/packages/rendering/src/canvas.ts index 6ae84f5ff..51642248b 100644 --- a/packages/rendering/src/canvas.ts +++ b/packages/rendering/src/canvas.ts @@ -43,21 +43,21 @@ function instrumentCanvasToBuffer() { try { result = toBuffer.apply(this, args) as ReturnType; } catch (error) { - span?.finish(); + span?.end(); throw error; } if (!result || typeof (result as Promise).then !== "function") { - span?.finish(); + span?.end(); return result; } return (result as Promise) .then((buffer) => { - span?.setData("png.bytes", buffer.byteLength); + span?.setAttribute("png.bytes", buffer.byteLength); return buffer; }) - .finally(() => span?.finish()) as ReturnType; + .finally(() => span?.end()) as ReturnType; }; } diff --git a/packages/rendering/src/jsx/render.ts b/packages/rendering/src/jsx/render.ts index c525c916a..990d83e35 100644 --- a/packages/rendering/src/jsx/render.ts +++ b/packages/rendering/src/jsx/render.ts @@ -6,7 +6,6 @@ * https://github.com/Statsify/statsify/blob/main/LICENSE */ -import * as Sentry from "@sentry/node"; import type { Canvas, CanvasRenderingContext2D } from "skia-canvas"; import { Container } from "typedi"; import { FontRenderer } from "#font"; From cf145f892c978fd6fe3e9165d3144b71fd37c3fd Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 22 Jun 2026 02:55:24 +0000 Subject: [PATCH 09/11] fix: remove duplicate lockfile importer entry --- pnpm-lock.yaml | 2 -- 1 file changed, 2 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e0ee130c6..940b3b887 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -660,8 +660,6 @@ importers: packages/skin-renderer/pkg: {} - packages/skin-renderer/pkg: {} - packages/util: dependencies: '@swc/helpers': From 80da83bb93fc52a63ffb1a1b498f9870d761bd75 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 22 Jun 2026 03:07:13 +0000 Subject: [PATCH 10/11] Fix inconsistent TypeScript snapshot in pnpm lockfile --- pnpm-lock.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4f2aec357..61578c2b0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -7790,8 +7790,8 @@ snapshots: is-glob: 4.0.3 minimatch: 9.0.5 semver: 7.7.3 - ts-api-utils: 2.1.0(typescript@5.9.2) - typescript: 5.9.2 + ts-api-utils: 2.1.0(typescript@6.0.3) + typescript: 6.0.3 transitivePeerDependencies: - supports-color From ad2a37fa53d3995fc4a100c2e5f7edfb1a442074 Mon Sep 17 00:00:00 2001 From: Cody Date: Sun, 21 Jun 2026 21:48:13 -0600 Subject: [PATCH 11/11] chore(pnpm): update node-cpu-profiler package name * Changed the package name from '@sentry-internal/node-cpu-profiler' to '@sentry/node-cpu-profiler' * Ensures consistency with the latest package naming conventions --- apps/api/package.json | 4 +- apps/api/src/index.ts | 1 + apps/discord-bot/package.json | 4 +- apps/discord-bot/src/index.ts | 7 +- apps/support-bot/package.json | 2 +- apps/support-bot/src/index.ts | 1 + apps/verify-server/package.json | 2 +- apps/verify-server/src/index.ts | 1 + packages/api-client/package.json | 2 +- packages/discord/package.json | 2 +- packages/logger/package.json | 2 +- packages/rendering/package.json | 2 +- pnpm-lock.yaml | 456 ++++++++++++++++++++++--------- pnpm-workspace.yaml | 2 +- 14 files changed, 349 insertions(+), 139 deletions(-) diff --git a/apps/api/package.json b/apps/api/package.json index a4ab97355..09f7e3189 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -20,8 +20,8 @@ "@nestjs/core": "^11.1.6", "@nestjs/platform-fastify": "^11.1.6", "@nestjs/swagger": "^11.2.0", - "@sentry/node": "^7.118.0", - "@sentry/profiling-node": "^7.120.4", + "@sentry/node": "^10.59.0", + "@sentry/profiling-node": "^10.59.0", "@statsify/api-client": "workspace:^", "@statsify/assets": "workspace:^", "@statsify/logger": "workspace:^", diff --git a/apps/api/src/index.ts b/apps/api/src/index.ts index acbbe9ad9..8336ecb30 100644 --- a/apps/api/src/index.ts +++ b/apps/api/src/index.ts @@ -46,6 +46,7 @@ if (sentryDsn) { nodeProfilingIntegration(), ], normalizeDepth: 3, + enableLogs: true, tracesSampleRate: sentryTracesSampleRate, profilesSampleRate: sentryProfilesSampleRate, environment: await config("environment"), diff --git a/apps/discord-bot/package.json b/apps/discord-bot/package.json index 55a856b5d..b597723b1 100644 --- a/apps/discord-bot/package.json +++ b/apps/discord-bot/package.json @@ -12,8 +12,8 @@ "lint:ci": "oxlint --format=github" }, "dependencies": { - "@sentry/node": "^7.118.0", - "@sentry/profiling-node": "^7.120.4", + "@sentry/node": "^10.59.0", + "@sentry/profiling-node": "^10.59.0", "@statsify/api-client": "workspace:^", "@statsify/assets": "workspace:^", "@statsify/discord": "workspace:^", diff --git a/apps/discord-bot/src/index.ts b/apps/discord-bot/src/index.ts index 2500730be..b71a6f029 100644 --- a/apps/discord-bot/src/index.ts +++ b/apps/discord-bot/src/index.ts @@ -40,9 +40,10 @@ if (sentryDsn) { Sentry.httpIntegration({ spans: false, breadcrumbs: true }), nodeProfilingIntegration(), ], - normalizeDepth: 3, - tracesSampleRate: sentryTracesSampleRate, - profilesSampleRate: sentryProfilesSampleRate, + normalizeDepth: 3, + enableLogs: true, + tracesSampleRate: sentryTracesSampleRate, + profilesSampleRate: sentryProfilesSampleRate, environment: await config("environment"), }); } diff --git a/apps/support-bot/package.json b/apps/support-bot/package.json index dab026189..276660cb9 100644 --- a/apps/support-bot/package.json +++ b/apps/support-bot/package.json @@ -12,7 +12,7 @@ "lint:ci": "oxlint --format=github" }, "dependencies": { - "@sentry/node": "^7.118.0", + "@sentry/node": "^10.59.0", "@statsify/api-client": "workspace:^", "@statsify/assets": "workspace:^", "@statsify/discord": "workspace:^", diff --git a/apps/support-bot/src/index.ts b/apps/support-bot/src/index.ts index ad1196cfb..f1da5bec3 100644 --- a/apps/support-bot/src/index.ts +++ b/apps/support-bot/src/index.ts @@ -46,6 +46,7 @@ if (sentryDsn) { dsn: sentryDsn, integrations: [Sentry.httpIntegration({ spans: false, breadcrumbs: true })], normalizeDepth: 3, + enableLogs: true, tracesSampleRate: await config("sentry.tracesSampleRate"), environment: await config("environment"), }); diff --git a/apps/verify-server/package.json b/apps/verify-server/package.json index 9d16b078c..a6b7fe6a5 100644 --- a/apps/verify-server/package.json +++ b/apps/verify-server/package.json @@ -12,7 +12,7 @@ "lint:ci": "oxlint --format=github" }, "dependencies": { - "@sentry/node": "^7.118.0", + "@sentry/node": "^10.59.0", "@statsify/assets": "workspace:^", "@statsify/logger": "workspace:^", "@statsify/schemas": "workspace:^", diff --git a/apps/verify-server/src/index.ts b/apps/verify-server/src/index.ts index c9134a808..46a0def8f 100644 --- a/apps/verify-server/src/index.ts +++ b/apps/verify-server/src/index.ts @@ -42,6 +42,7 @@ if (sentryDsn) { dsn: sentryDsn, integrations: [Sentry.mongoIntegration()], normalizeDepth: 3, + enableLogs: true, tracesSampleRate: await config("sentry.tracesSampleRate"), environment: await config("environment"), }); diff --git a/packages/api-client/package.json b/packages/api-client/package.json index 0cd661b49..624c1a94a 100644 --- a/packages/api-client/package.json +++ b/packages/api-client/package.json @@ -13,7 +13,7 @@ "dependencies": { "@nestjs/common": "^11.1.6", "@nestjs/swagger": "^11.2.0", - "@sentry/node": "^7.118.0", + "@sentry/node": "^10.59.0", "@statsify/logger": "workspace:^", "@statsify/rendering": "workspace:^", "@statsify/schemas": "workspace:^", diff --git a/packages/discord/package.json b/packages/discord/package.json index eae37fb57..6ac5921ec 100644 --- a/packages/discord/package.json +++ b/packages/discord/package.json @@ -11,7 +11,7 @@ "lint:ci": "oxlint --format=github" }, "dependencies": { - "@sentry/node": "^7.118.0", + "@sentry/node": "^10.59.0", "@statsify/api-client": "workspace:^", "@statsify/assets": "workspace:^", "@statsify/logger": "workspace:^", diff --git a/packages/logger/package.json b/packages/logger/package.json index f40ff474e..010008646 100644 --- a/packages/logger/package.json +++ b/packages/logger/package.json @@ -11,7 +11,7 @@ "lint:ci": "oxlint --format=github" }, "dependencies": { - "@sentry/node": "^7.118.0", + "@sentry/node": "^10.59.0", "@statsify/util": "workspace:^", "@swc/helpers": "^0.5.23", "chalk": "5.6.0", diff --git a/packages/rendering/package.json b/packages/rendering/package.json index 737958049..9ea3cbd86 100644 --- a/packages/rendering/package.json +++ b/packages/rendering/package.json @@ -20,7 +20,7 @@ "lint:ci": "oxlint --format=github" }, "dependencies": { - "@sentry/node": "^7.118.0", + "@sentry/node": "^10.59.0", "@statsify/assets": "workspace:^", "@statsify/logger": "workspace:^", "@statsify/util": "workspace:^", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 61578c2b0..feb17abba 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -58,7 +58,7 @@ importers: version: 3.0.0 vitest: specifier: ^4.1.8 - version: 4.1.8(@types/node@25.9.1)(@vitest/coverage-v8@4.1.8)(@vitest/ui@4.1.8)(vite@7.0.4(@types/node@25.9.1)(jiti@2.7.0)(lightningcss@1.32.0)) + version: 4.1.8(@opentelemetry/api@1.9.1)(@types/node@25.9.1)(@vitest/coverage-v8@4.1.8)(@vitest/ui@4.1.8)(vite@7.0.4(@types/node@25.9.1)(jiti@2.7.0)(lightningcss@1.32.0)) apps/api: dependencies: @@ -87,11 +87,11 @@ importers: specifier: ^11.2.0 version: 11.2.0(@fastify/static@8.2.0)(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.6(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2))(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2) '@sentry/node': - specifier: ^7.118.0 - version: 7.120.4 + specifier: ^10.59.0 + version: 10.59.0(vite@7.0.4(@types/node@25.9.1)(jiti@2.7.0)(lightningcss@1.32.0)) '@sentry/profiling-node': - specifier: ^7.120.4 - version: 7.120.4 + specifier: ^10.59.0 + version: 10.59.0(vite@7.0.4(@types/node@25.9.1)(jiti@2.7.0)(lightningcss@1.32.0)) '@statsify/api-client': specifier: workspace:^ version: link:../../packages/api-client @@ -163,11 +163,11 @@ importers: apps/discord-bot: dependencies: '@sentry/node': - specifier: ^7.118.0 - version: 7.120.4 + specifier: ^10.59.0 + version: 10.59.0(vite@7.0.4(@types/node@25.9.1)(jiti@2.7.0)(lightningcss@1.32.0)) '@sentry/profiling-node': - specifier: ^7.120.4 - version: 7.120.4 + specifier: ^10.59.0 + version: 10.59.0(vite@7.0.4(@types/node@25.9.1)(jiti@2.7.0)(lightningcss@1.32.0)) '@statsify/api-client': specifier: workspace:^ version: link:../../packages/api-client @@ -298,10 +298,10 @@ importers: version: 5.90.12(react@19.2.1) '@vercel/analytics': specifier: ^1.6.1 - version: 1.6.1(next@16.0.7(react-dom@19.2.1(react@19.2.1))(react@19.2.1))(react@19.2.1) + version: 1.6.1(next@16.0.7(@opentelemetry/api@1.9.1)(react-dom@19.2.1(react@19.2.1))(react@19.2.1))(react@19.2.1) '@vercel/speed-insights': specifier: ^2.0.0 - version: 2.0.0(next@16.0.7(react-dom@19.2.1(react@19.2.1))(react@19.2.1))(react@19.2.1) + version: 2.0.0(next@16.0.7(@opentelemetry/api@1.9.1)(react-dom@19.2.1(react@19.2.1))(react@19.2.1))(react@19.2.1) date-fns: specifier: ^4.1.0 version: 4.1.0 @@ -310,7 +310,7 @@ importers: version: 12.23.25(react-dom@19.2.1(react@19.2.1))(react@19.2.1) next: specifier: ^16.0.7 - version: 16.0.7(react-dom@19.2.1(react@19.2.1))(react@19.2.1) + version: 16.0.7(@opentelemetry/api@1.9.1)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) react: specifier: ^19.2.1 version: 19.2.1 @@ -355,8 +355,8 @@ importers: apps/support-bot: dependencies: '@sentry/node': - specifier: ^7.118.0 - version: 7.120.4 + specifier: ^10.59.0 + version: 10.59.0(vite@7.0.4(@types/node@25.9.1)(jiti@2.7.0)(lightningcss@1.32.0)) '@statsify/api-client': specifier: workspace:^ version: link:../../packages/api-client @@ -419,8 +419,8 @@ importers: apps/verify-server: dependencies: '@sentry/node': - specifier: ^7.118.0 - version: 7.120.4 + specifier: ^10.59.0 + version: 10.59.0(vite@7.0.4(@types/node@25.9.1)(jiti@2.7.0)(lightningcss@1.32.0)) '@statsify/assets': specifier: workspace:^ version: link:../../packages/assets @@ -446,6 +446,24 @@ importers: specifier: ^8.5.2 version: 8.18.0(socks@2.8.7) + assets/private: + dependencies: + skia-canvas: + specifier: 3.0.8 + version: 3.0.8 + stackblur-canvas: + specifier: ^2.7.0 + version: 2.7.0 + + assets/public: + dependencies: + skia-canvas: + specifier: 3.0.8 + version: 3.0.8 + stackblur-canvas: + specifier: 2.7.0 + version: 2.7.0 + packages/api-client: dependencies: '@nestjs/common': @@ -455,8 +473,8 @@ importers: specifier: ^11.2.0 version: 11.2.0(@fastify/static@8.2.0)(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.6(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2))(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2) '@sentry/node': - specifier: ^7.118.0 - version: 7.120.4 + specifier: ^10.59.0 + version: 10.59.0(vite@7.0.4(@types/node@25.9.1)(jiti@2.7.0)(lightningcss@1.32.0)) '@statsify/logger': specifier: workspace:^ version: link:../logger @@ -497,8 +515,8 @@ importers: packages/discord: dependencies: '@sentry/node': - specifier: ^7.118.0 - version: 7.120.4 + specifier: ^10.59.0 + version: 10.59.0(vite@7.0.4(@types/node@25.9.1)(jiti@2.7.0)(lightningcss@1.32.0)) '@statsify/api-client': specifier: workspace:^ version: link:../api-client @@ -549,8 +567,8 @@ importers: packages/logger: dependencies: '@sentry/node': - specifier: ^7.118.0 - version: 7.120.4 + specifier: ^10.59.0 + version: 10.59.0(vite@7.0.4(@types/node@25.9.1)(jiti@2.7.0)(lightningcss@1.32.0)) '@statsify/util': specifier: workspace:^ version: link:../util @@ -583,8 +601,8 @@ importers: packages/rendering: dependencies: '@sentry/node': - specifier: ^7.118.0 - version: 7.120.4 + specifier: ^10.59.0 + version: 10.59.0(vite@7.0.4(@types/node@25.9.1)(jiti@2.7.0)(lightningcss@1.32.0)) '@statsify/assets': specifier: workspace:^ version: link:../assets @@ -640,6 +658,8 @@ importers: packages/skin-renderer: {} + packages/skin-renderer/pkg: {} + packages/util: dependencies: '@swc/helpers': @@ -652,6 +672,17 @@ packages: resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} engines: {node: '>=10'} + '@apm-js-collab/code-transformer-bundler-plugins@0.5.0': + resolution: {integrity: sha512-YxLBY5nGlurL7QeJLq6e5g0ouBpAp0pwgyA/5rHXEXwhiPLn9ZHbT+Y2LlP90GT872cSocfjWRYu/fnpuBudNQ==} + engines: {node: '>=18.0.0'} + + '@apm-js-collab/code-transformer@0.15.0': + resolution: {integrity: sha512-XmXYVs8CzJ1Aj79noVbn2weUO/XWtRyURpGqx7aU7DOXlUQhR0WKOQNF0okh7PCeY37vxf7kU3v57OAkEPm3ww==} + hasBin: true + + '@apm-js-collab/tracing-hooks@0.10.0': + resolution: {integrity: sha512-2/Z3NTewJTruUkmsSnBC5bJlLNUd9keuD1OLlTEpim4FyLhm6m2Rnfv+wrFdUvFfhmH8CRdiDZBqBrn+wyaGuA==} + '@azure/msal-common@14.16.0': resolution: {integrity: sha512-1KOZj9IpcDSwpNiQNjt0jDYZpQvNZay7QAEi/5DLubay40iGYtLzya/jbjRPLyOTZhEKyL1MzPuw2HqBCjceYA==} engines: {node: '>=0.8.0'} @@ -2024,6 +2055,42 @@ packages: '@octokit/types@16.0.0': resolution: {integrity: sha512-sKq+9r1Mm4efXW1FCk7hFSeJo4QKreL/tTbR0rz/qx/r1Oa2VV83LTA/H/MuCOX7uCIJmQVRKBcbmWoySjAnSg==} + '@opentelemetry/api-logs@0.214.0': + resolution: {integrity: sha512-40lSJeqYO8Uz2Yj7u94/SJWE/wONa7rmMKjI1ZcIjgf3MHNHv1OZUCrCETGuaRF62d5pQD1wKIW+L4lmSMTzZA==} + engines: {node: '>=8.0.0'} + + '@opentelemetry/api@1.9.1': + resolution: {integrity: sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q==} + engines: {node: '>=8.0.0'} + + '@opentelemetry/core@2.8.0': + resolution: {integrity: sha512-hd1Lfh8p545nNz+jq1Ejfz+Mn1hyLuxYn1YzTfFNrxr8urEWMNQLPf1Th8kjOH+HxwawCrtgBp8JpBUR4ZSgww==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/instrumentation@0.214.0': + resolution: {integrity: sha512-MHqEX5Dk59cqVah5LiARMACku7jXSVk9iVDWOea4x3cr7VfdByeDCURK6o1lntT1JS/Tsovw01UJrBhN3/uC5w==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/resources@2.8.0': + resolution: {integrity: sha512-qmXQ27ilDbUK/vGMqwL8D4/rhn76C+sherM4wTbjlfknR8Nvfc/hCxjRJPhkzZzUsPiNg16SA31NxMabwttRjg==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.3.0 <1.10.0' + + '@opentelemetry/sdk-trace-base@2.8.0': + resolution: {integrity: sha512-mhU4jp+vW0mGbFRd+GeXHvmfA4aDqWjBjLC3pE5XMpLs0IE2ryYb019Ts2AQrOq67gaTF25D91+fgvEHDZEnuQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.3.0 <1.10.0' + + '@opentelemetry/semantic-conventions@1.41.1': + resolution: {integrity: sha512-/UhIkaZgPutTFmQ7RnIJGgDXZmtEJ7Dvi86xNTFWcnRxVRNk/aotsqDJYeEvDP+FSMB2SdW+pQzNMcWP0rwuNA==} + engines: {node: '>=14'} + '@oxfmt/binding-android-arm-eabi@0.53.0': resolution: {integrity: sha512-XfVM8AmIovBTKXCt14Op5wbfcoM8418nttd+nhMgM3RAVaJg1MtJc73FyWfUt0oxLyBGVwfniNVUsbV/b3VmPg==} engines: {node: ^20.19.0 || >=22.12.0} @@ -2666,34 +2733,64 @@ packages: '@sec-ant/readable-stream@0.4.1': resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==} - '@sentry-internal/tracing@7.120.4': - resolution: {integrity: sha512-Fz5+4XCg3akeoFK+K7g+d7HqGMjmnLoY2eJlpONJmaeT9pXY7yfUyXKZMmMajdE2LxxKJgQ2YKvSCaGVamTjHw==} - engines: {node: '>=8'} + '@sentry/conventions@0.12.0': + resolution: {integrity: sha512-z1JQrl/1SLY+8wpzvork6vl+fpsg/oCCxM7HWWhUnI/R+OGNyoIzieQuggX3uUMY7NBtp8UWCQx6FeFazzOF9g==} + engines: {node: '>=14'} - '@sentry/core@7.120.4': - resolution: {integrity: sha512-TXu3Q5kKiq8db9OXGkWyXUbIxMMuttB5vJ031yolOl5T/B69JRyAoKuojLBjRv1XX583gS1rSSoX8YXX7ATFGA==} - engines: {node: '>=8'} + '@sentry/core@10.59.0': + resolution: {integrity: sha512-QeG7XZL5j6CkToYCE7OwCerb/r742Tjj9p1BBohBKcypYTPRuqfD+A3FeUj7pk5CGO6Vj1/gOAmdbuuNbR51dQ==} + engines: {node: '>=18'} - '@sentry/integrations@7.120.4': - resolution: {integrity: sha512-kkBTLk053XlhDCg7OkBQTIMF4puqFibeRO3E3YiVc4PGLnocXMaVpOSCkMqAc1k1kZ09UgGi8DxfQhnFEjUkpA==} - engines: {node: '>=8'} + '@sentry/node-core@10.59.0': + resolution: {integrity: sha512-qFbepzntYhDleNG9ZCZWCSoAJK0Nsx+UJxsuiygaaAf1rJMj95RVckLyslhY86pyDLVATNMmWm2elm6etgKaJw==} + engines: {node: '>=18'} + peerDependencies: + '@opentelemetry/api': ^1.9.0 + '@opentelemetry/core': ^1.30.1 || ^2.1.0 + '@opentelemetry/exporter-trace-otlp-http': '>=0.57.0 <1' + '@opentelemetry/instrumentation': '>=0.57.1 <1' + '@opentelemetry/sdk-trace-base': ^1.30.1 || ^2.1.0 + peerDependenciesMeta: + '@opentelemetry/api': + optional: true + '@opentelemetry/core': + optional: true + '@opentelemetry/exporter-trace-otlp-http': + optional: true + '@opentelemetry/instrumentation': + optional: true + '@opentelemetry/sdk-trace-base': + optional: true - '@sentry/node@7.120.4': - resolution: {integrity: sha512-qq3wZAXXj2SRWhqErnGCSJKUhPSlZ+RGnCZjhfjHpP49KNpcd9YdPTIUsFMgeyjdh6Ew6aVCv23g1hTP0CHpYw==} - engines: {node: '>=8'} + '@sentry/node-cpu-profiler@2.4.2': + resolution: {integrity: sha512-E6q+eE/sTpiofzW9jFKAx6ZQaDAoZDnsaLA/nRlkiK+K2X4k+hSyKhhLfw8PJlejB8edk7uxJF57r5JoRnyaPA==} + engines: {node: '>=18'} - '@sentry/profiling-node@7.120.4': - resolution: {integrity: sha512-2Eb/LcYk7ohUx1KNnxcrN6hiyFTbD8Q9ffAvqtx09yJh1JhasvA+XCAcY72ONI5Aia4rCVkql9eEPSyhkmhsbA==} - engines: {node: '>=8.0.0'} - hasBin: true + '@sentry/node@10.59.0': + resolution: {integrity: sha512-qzqbP6OVoMijlDBUxWtbvVF5j73+vyzGFi+yFIslhVvzBj97TFkIeP3TpBLsmu/0L5ZvxpQCCEmzJ677tFkq/g==} + engines: {node: '>=18'} - '@sentry/types@7.120.4': - resolution: {integrity: sha512-cUq2hSSe6/qrU6oZsEP4InMI5VVdD86aypE+ENrQ6eZEVLTCYm1w6XhW1NvIu3UuWh7gZec4a9J7AFpYxki88Q==} - engines: {node: '>=8'} + '@sentry/opentelemetry@10.59.0': + resolution: {integrity: sha512-wV9/HR9btrNhSkJC2S0urqsD9pE4K0f6AmdfTK3qhH505mLoyV4ekTG66hdDR9xD2zOYCm58CNzaK+336zu3Gg==} + engines: {node: '>=18'} + peerDependencies: + '@opentelemetry/api': ^1.9.0 + '@opentelemetry/core': ^1.30.1 || ^2.1.0 + '@opentelemetry/sdk-trace-base': ^1.30.1 || ^2.1.0 - '@sentry/utils@7.120.4': - resolution: {integrity: sha512-zCKpyDIWKHwtervNK2ZlaK8mMV7gVUijAgFeJStH+CU/imcdquizV3pFLlSQYRswG+Lbyd6CT/LGRh3IbtkCFw==} - engines: {node: '>=8'} + '@sentry/profiling-node@10.59.0': + resolution: {integrity: sha512-dhi5IsGv3B3U/vg+AvnzBI+kH5ZVP6CsB9QtFY0LRl18AmAr3Af0f/3dtK2Wr6rOuQb+nxSXRhKlisEQ1EL7vQ==} + engines: {node: '>=18'} + hasBin: true + + '@sentry/server-utils@10.59.0': + resolution: {integrity: sha512-mR3fWaU7uGxIstRba6YO+/6V3qIa7432F7/U8EWHry+dY4C9DWAVG90E2GCzeD2MwLSP0tB25i8p1TWTGiQgVg==} + engines: {node: '>=18'} + peerDependencies: + vite: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 + peerDependenciesMeta: + vite: + optional: true '@sindresorhus/is@7.2.0': resolution: {integrity: sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw==} @@ -3254,6 +3351,11 @@ packages: abstract-logging@2.0.1: resolution: {integrity: sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==} + acorn-import-attributes@1.9.5: + resolution: {integrity: sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==} + peerDependencies: + acorn: ^8 + acorn-jsx@5.3.2: resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: @@ -3344,6 +3446,10 @@ packages: ast-v8-to-istanbul@1.0.3: resolution: {integrity: sha512-jCMQ6ZylLPudp0CDfBmQBZUsrh1/8psbmu9ibeVWKuHWD0YrH9YABwlKu5kVEFoT0GCQQW9Z/SxfuEbbkGQCRg==} + astring@1.9.0: + resolution: {integrity: sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg==} + hasBin: true + async@2.6.4: resolution: {integrity: sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==} @@ -3501,6 +3607,9 @@ packages: resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} engines: {node: '>= 8.10.0'} + cjs-module-lexer@2.2.0: + resolution: {integrity: sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ==} + class-transformer@0.5.1: resolution: {integrity: sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==} @@ -4177,13 +4286,14 @@ packages: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} - immediate@3.0.6: - resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==} - import-fresh@3.3.1: resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} engines: {node: '>=6'} + import-in-the-middle@3.1.0: + resolution: {integrity: sha512-c0AeAV8VcwZzfYE7euTZY3H+VXUPMVugiovdosq80lqEXJmOekg3zGUAYg6KImHMaMuBoTUfTv7xNpUFdy0hJA==} + engines: {node: '>=18'} + imurmurhash@0.1.4: resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} engines: {node: '>=0.8.19'} @@ -4361,9 +4471,6 @@ packages: libphonenumber-js@1.12.10: resolution: {integrity: sha512-E91vHJD61jekHHR/RF/E83T/CMoaLXT7cwYA75T4gim4FZjnM6hbJjVIGg7chqlSqRsSvQ3izGmOjHy1SQzcGQ==} - lie@3.1.1: - resolution: {integrity: sha512-RiNhHysUjhrDQntfYSfY4MU24coXXdEOgw9WGcKHNeEwffDYbF//u87M1EWaMGzuFoSbqW0C9C6lEEhDOAswfw==} - light-my-request@6.6.0: resolution: {integrity: sha512-CHYbu8RtboSIoVsHZ6Ye4cj4Aw/yg2oAFimlF7mNvfDV192LR7nDiKtSIfCuLT7KokPSTn/9kfVLm5OGN0A28A==} @@ -4441,9 +4548,6 @@ packages: resolution: {integrity: sha512-nVAvWk/jeyrWyXEAs84mpQCYccxRqgKY4OznLuJhJCa0XsPSfdOIr2zvBZEj3IHEHbX97jjscKRRV539bW0Gpw==} engines: {node: '>=13.2.0'} - localforage@1.10.0: - resolution: {integrity: sha512-14/H1aX7hzBBmmh7sGPd+AOMkkIrHM3Z1PAyGgZigA1H1p5O5ANnMyWzvpAETtG68/dC4pC0ncy3+PPGzXZHPg==} - locate-path@6.0.0: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} @@ -4539,6 +4643,10 @@ packages: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} + meriyah@6.1.4: + resolution: {integrity: sha512-Sz8FzjzI0kN13GK/6MVEsVzMZEPvOhnmmI1lU5+/1cGOiK3QUahntrNNtdVeihrO7t9JpoH75iMNXg6R6uWflQ==} + engines: {node: '>=18.0.0'} + micromatch@4.0.8: resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} engines: {node: '>=8.6'} @@ -5121,6 +5229,10 @@ packages: resolution: {integrity: sha512-efCx3b+0Z69/LGJmm9Yvi4cqEdxnoGnxYxGxBghkkTTFeXRtTCmmhO0AnAfHz59k957uTSuy8WaHqOs8wbYUWg==} engines: {node: '>=6'} + 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'} + requireindex@1.2.0: resolution: {integrity: sha512-L9jEkOi3ASd9PYit2cwRfyppc9NoABujTP8/5gFcbERmo5jUoAKovIC3fsF17pkTnGsrByysqX+Kxd2OTNI1ww==} engines: {node: '>=0.10.5'} @@ -5200,6 +5312,9 @@ packages: resolution: {integrity: sha512-SMguiTnYrhpLdk3PwfzHeotrcwi8bNV4iemL9tx9poR/yeaMYwB9VzR1w7b57DuWpuqR8n6oZboi0hj3AxZxQg==} hasBin: true + semifies@1.0.0: + resolution: {integrity: sha512-xXR3KGeoxTNWPD4aBvL5NUpMTT7WMANr3EWnaS190QVkY52lqqcVRD7Q05UVbBhiWDGWMlJEUam9m7uFFGVScw==} + semver-regex@4.0.5: resolution: {integrity: sha512-hunMQrEy1T6Jr2uEVjrAIqjwWcQTgOAcIM52C8MY1EZSD3DDNft04XzvYKPqjED65bNVVko0YI38nYeEHCX3yw==} engines: {node: '>=12'} @@ -5324,6 +5439,10 @@ packages: stackback@0.0.2: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + stackblur-canvas@2.7.0: + resolution: {integrity: sha512-yf7OENo23AGJhBriGx0QivY5JP6Y1HbrrDI6WLt6C5auYZXlQrheoY8hD4ibekFKz1HOfE48Ww8kMWMnJD/zcQ==} + engines: {node: '>=0.1.14'} + standard-as-callback@2.1.0: resolution: {integrity: sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==} @@ -5796,6 +5915,30 @@ snapshots: '@alloc/quick-lru@5.2.0': {} + '@apm-js-collab/code-transformer-bundler-plugins@0.5.0': + dependencies: + '@apm-js-collab/code-transformer': 0.15.0 + es-module-lexer: 2.1.0 + magic-string: 0.30.21 + module-details-from-path: 1.0.4 + + '@apm-js-collab/code-transformer@0.15.0': + dependencies: + '@types/estree': 1.0.9 + astring: 1.9.0 + esquery: 1.7.0 + meriyah: 6.1.4 + semifies: 1.0.0 + source-map: 0.6.1 + + '@apm-js-collab/tracing-hooks@0.10.0': + dependencies: + '@apm-js-collab/code-transformer': 0.15.0 + debug: 4.4.3 + module-details-from-path: 1.0.4 + transitivePeerDependencies: + - supports-color + '@azure/msal-common@14.16.0': {} '@azure/msal-node@2.16.2': @@ -6972,6 +7115,41 @@ snapshots: dependencies: '@octokit/openapi-types': 27.0.0 + '@opentelemetry/api-logs@0.214.0': + dependencies: + '@opentelemetry/api': 1.9.1 + + '@opentelemetry/api@1.9.1': {} + + '@opentelemetry/core@2.8.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/semantic-conventions': 1.41.1 + + '@opentelemetry/instrumentation@0.214.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/api-logs': 0.214.0 + import-in-the-middle: 3.1.0 + require-in-the-middle: 8.0.1 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/resources@2.8.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.8.0(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.41.1 + + '@opentelemetry/sdk-trace-base@2.8.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.8.0(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 2.8.0(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.41.1 + + '@opentelemetry/semantic-conventions@1.41.1': {} + '@oxfmt/binding-android-arm-eabi@0.53.0': optional: true @@ -7437,42 +7615,74 @@ snapshots: '@sec-ant/readable-stream@0.4.1': {} - '@sentry-internal/tracing@7.120.4': - dependencies: - '@sentry/core': 7.120.4 - '@sentry/types': 7.120.4 - '@sentry/utils': 7.120.4 - - '@sentry/core@7.120.4': - dependencies: - '@sentry/types': 7.120.4 - '@sentry/utils': 7.120.4 + '@sentry/conventions@0.12.0': {} - '@sentry/integrations@7.120.4': - dependencies: - '@sentry/core': 7.120.4 - '@sentry/types': 7.120.4 - '@sentry/utils': 7.120.4 - localforage: 1.10.0 + '@sentry/core@10.59.0': {} - '@sentry/node@7.120.4': + '@sentry/node-core@10.59.0(@opentelemetry/api@1.9.1)(@opentelemetry/core@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/instrumentation@0.214.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.8.0(@opentelemetry/api@1.9.1))': dependencies: - '@sentry-internal/tracing': 7.120.4 - '@sentry/core': 7.120.4 - '@sentry/integrations': 7.120.4 - '@sentry/types': 7.120.4 - '@sentry/utils': 7.120.4 + '@sentry/conventions': 0.12.0 + '@sentry/core': 10.59.0 + '@sentry/opentelemetry': 10.59.0(@opentelemetry/api@1.9.1)(@opentelemetry/core@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.8.0(@opentelemetry/api@1.9.1)) + import-in-the-middle: 3.1.0 + optionalDependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.8.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation': 0.214.0(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-trace-base': 2.8.0(@opentelemetry/api@1.9.1) - '@sentry/profiling-node@7.120.4': + '@sentry/node-cpu-profiler@2.4.2': dependencies: detect-libc: 2.1.2 node-abi: 3.92.0 - '@sentry/types@7.120.4': {} + '@sentry/node@10.59.0(vite@7.0.4(@types/node@25.9.1)(jiti@2.7.0)(lightningcss@1.32.0))': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.8.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation': 0.214.0(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-trace-base': 2.8.0(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.41.1 + '@sentry/core': 10.59.0 + '@sentry/node-core': 10.59.0(@opentelemetry/api@1.9.1)(@opentelemetry/core@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/instrumentation@0.214.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.8.0(@opentelemetry/api@1.9.1)) + '@sentry/opentelemetry': 10.59.0(@opentelemetry/api@1.9.1)(@opentelemetry/core@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.8.0(@opentelemetry/api@1.9.1)) + '@sentry/server-utils': 10.59.0(vite@7.0.4(@types/node@25.9.1)(jiti@2.7.0)(lightningcss@1.32.0)) + import-in-the-middle: 3.1.0 + transitivePeerDependencies: + - '@opentelemetry/exporter-trace-otlp-http' + - supports-color + - vite + + '@sentry/opentelemetry@10.59.0(@opentelemetry/api@1.9.1)(@opentelemetry/core@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.8.0(@opentelemetry/api@1.9.1))': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.8.0(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-trace-base': 2.8.0(@opentelemetry/api@1.9.1) + '@sentry/conventions': 0.12.0 + '@sentry/core': 10.59.0 - '@sentry/utils@7.120.4': + '@sentry/profiling-node@10.59.0(vite@7.0.4(@types/node@25.9.1)(jiti@2.7.0)(lightningcss@1.32.0))': dependencies: - '@sentry/types': 7.120.4 + '@sentry/core': 10.59.0 + '@sentry/node': 10.59.0(vite@7.0.4(@types/node@25.9.1)(jiti@2.7.0)(lightningcss@1.32.0)) + '@sentry/node-cpu-profiler': 2.4.2 + transitivePeerDependencies: + - '@opentelemetry/exporter-trace-otlp-http' + - supports-color + - vite + + '@sentry/server-utils@10.59.0(vite@7.0.4(@types/node@25.9.1)(jiti@2.7.0)(lightningcss@1.32.0))': + dependencies: + '@apm-js-collab/code-transformer': 0.15.0 + '@apm-js-collab/code-transformer-bundler-plugins': 0.5.0 + '@apm-js-collab/tracing-hooks': 0.10.0 + '@sentry/conventions': 0.12.0 + '@sentry/core': 10.59.0 + magic-string: 0.30.21 + optionalDependencies: + vite: 7.0.4(@types/node@25.9.1)(jiti@2.7.0)(lightningcss@1.32.0) + transitivePeerDependencies: + - supports-color '@sindresorhus/is@7.2.0': {} @@ -7842,14 +8052,14 @@ snapshots: '@typescript/native-preview-win32-arm64': 7.0.0-dev.20260601.1 '@typescript/native-preview-win32-x64': 7.0.0-dev.20260601.1 - '@vercel/analytics@1.6.1(next@16.0.7(react-dom@19.2.1(react@19.2.1))(react@19.2.1))(react@19.2.1)': + '@vercel/analytics@1.6.1(next@16.0.7(@opentelemetry/api@1.9.1)(react-dom@19.2.1(react@19.2.1))(react@19.2.1))(react@19.2.1)': optionalDependencies: - next: 16.0.7(react-dom@19.2.1(react@19.2.1))(react@19.2.1) + next: 16.0.7(@opentelemetry/api@1.9.1)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) react: 19.2.1 - '@vercel/speed-insights@2.0.0(next@16.0.7(react-dom@19.2.1(react@19.2.1))(react@19.2.1))(react@19.2.1)': + '@vercel/speed-insights@2.0.0(next@16.0.7(@opentelemetry/api@1.9.1)(react-dom@19.2.1(react@19.2.1))(react@19.2.1))(react@19.2.1)': optionalDependencies: - next: 16.0.7(react-dom@19.2.1(react@19.2.1))(react@19.2.1) + next: 16.0.7(@opentelemetry/api@1.9.1)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) react: 19.2.1 '@vitest/coverage-v8@4.1.8(vitest@4.1.8)': @@ -7864,7 +8074,7 @@ snapshots: obug: 2.1.1 std-env: 4.1.0 tinyrainbow: 3.1.0 - vitest: 4.1.8(@types/node@25.9.1)(@vitest/coverage-v8@4.1.8)(@vitest/ui@4.1.8)(vite@7.0.4(@types/node@25.9.1)(jiti@2.7.0)(lightningcss@1.32.0)) + vitest: 4.1.8(@opentelemetry/api@1.9.1)(@types/node@25.9.1)(@vitest/coverage-v8@4.1.8)(@vitest/ui@4.1.8)(vite@7.0.4(@types/node@25.9.1)(jiti@2.7.0)(lightningcss@1.32.0)) '@vitest/expect@4.1.8': dependencies: @@ -7910,7 +8120,7 @@ snapshots: sirv: 3.0.2 tinyglobby: 0.2.17 tinyrainbow: 3.1.0 - vitest: 4.1.8(@types/node@25.9.1)(@vitest/coverage-v8@4.1.8)(@vitest/ui@4.1.8)(vite@7.0.4(@types/node@25.9.1)(jiti@2.7.0)(lightningcss@1.32.0)) + vitest: 4.1.8(@opentelemetry/api@1.9.1)(@types/node@25.9.1)(@vitest/coverage-v8@4.1.8)(@vitest/ui@4.1.8)(vite@7.0.4(@types/node@25.9.1)(jiti@2.7.0)(lightningcss@1.32.0)) '@vitest/utils@4.1.8': dependencies: @@ -8027,6 +8237,10 @@ snapshots: abstract-logging@2.0.1: {} + acorn-import-attributes@1.9.5(acorn@8.16.0): + dependencies: + acorn: 8.16.0 + acorn-jsx@5.3.2(acorn@8.16.0): dependencies: acorn: 8.16.0 @@ -8104,6 +8318,8 @@ snapshots: estree-walker: 3.0.3 js-tokens: 10.0.0 + astring@1.9.0: {} + async@2.6.4: dependencies: lodash: 4.17.21 @@ -8252,6 +8468,8 @@ snapshots: optionalDependencies: fsevents: 2.3.3 + cjs-module-lexer@2.2.0: {} + class-transformer@0.5.1: {} class-validator@0.14.2: @@ -8969,13 +9187,18 @@ snapshots: ignore@5.3.2: {} - immediate@3.0.6: {} - import-fresh@3.3.1: dependencies: parent-module: 1.0.1 resolve-from: 4.0.0 + import-in-the-middle@3.1.0: + dependencies: + acorn: 8.16.0 + acorn-import-attributes: 1.9.5(acorn@8.16.0) + cjs-module-lexer: 2.2.0 + module-details-from-path: 1.0.4 + imurmurhash@0.1.4: {} inherits@2.0.4: {} @@ -9019,21 +9242,6 @@ snapshots: dependencies: binary-extensions: 2.3.0 - is-boolean-object@1.2.2: - dependencies: - call-bound: 1.0.4 - has-tostringtag: 1.0.2 - - is-builtin-module@5.0.0: - dependencies: - builtin-modules: 5.0.0 - - is-bun-module@2.0.0: - dependencies: - semver: 7.7.3 - - is-callable@1.2.7: {} - is-class@0.0.9: {} is-core-module@2.16.1: @@ -9136,13 +9344,6 @@ snapshots: ms: 2.1.3 semver: 7.7.3 - jsx-ast-utils@3.3.5: - dependencies: - array-includes: 3.1.9 - array.prototype.flat: 1.3.3 - object.assign: 4.1.7 - object.values: 1.2.1 - jwa@1.4.2: dependencies: buffer-equal-constant-time: 1.0.1 @@ -9173,10 +9374,6 @@ snapshots: libphonenumber-js@1.12.10: {} - lie@3.1.1: - dependencies: - immediate: 3.0.6 - light-my-request@6.6.0: dependencies: cookie: 1.0.2 @@ -9234,10 +9431,6 @@ snapshots: load-esm@1.0.2: {} - localforage@1.10.0: - dependencies: - lie: 3.1.1 - locate-path@6.0.0: dependencies: p-locate: 5.0.0 @@ -9310,6 +9503,8 @@ snapshots: merge2@1.4.1: {} + meriyah@6.1.4: {} + micromatch@4.0.8: dependencies: braces: 3.0.3 @@ -9438,7 +9633,7 @@ snapshots: mquery@5.0.0: dependencies: - debug: 4.4.1 + debug: 4.4.3 transitivePeerDependencies: - supports-color @@ -9477,7 +9672,7 @@ snapshots: netmask@2.0.2: {} - next@16.0.7(react-dom@19.2.1(react@19.2.1))(react@19.2.1): + next@16.0.7(@opentelemetry/api@1.9.1)(react-dom@19.2.1(react@19.2.1))(react@19.2.1): dependencies: '@next/env': 16.0.7 '@swc/helpers': 0.5.15 @@ -9495,6 +9690,7 @@ snapshots: '@next/swc-linux-x64-musl': 16.0.7 '@next/swc-win32-arm64-msvc': 16.0.7 '@next/swc-win32-x64-msvc': 16.0.7 + '@opentelemetry/api': 1.9.1 sharp: 0.34.5 transitivePeerDependencies: - '@babel/core' @@ -9502,7 +9698,7 @@ snapshots: node-abi@3.92.0: dependencies: - semver: 7.7.3 + semver: 7.8.1 node-fetch@2.7.0: dependencies: @@ -9969,6 +10165,13 @@ snapshots: transitivePeerDependencies: - supports-color + require-in-the-middle@8.0.1: + dependencies: + debug: 4.4.3 + module-details-from-path: 1.0.4 + transitivePeerDependencies: + - supports-color + requireindex@1.2.0: {} resolve-alpn@1.2.1: {} @@ -10056,14 +10259,14 @@ snapshots: dependencies: commander: 6.2.1 + semifies@1.0.0: {} + semver-regex@4.0.5: {} semver-truncate@3.0.0: dependencies: semver: 7.7.3 - semver@6.3.1: {} - semver@7.5.4: dependencies: lru-cache: 6.0.0 @@ -10192,6 +10395,8 @@ snapshots: stackback@0.0.2: {} + stackblur-canvas@2.7.0: {} + standard-as-callback@2.1.0: {} statuses@2.0.1: {} @@ -10469,7 +10674,7 @@ snapshots: jiti: 2.7.0 lightningcss: 1.32.0 - vitest@4.1.8(@types/node@25.9.1)(@vitest/coverage-v8@4.1.8)(@vitest/ui@4.1.8)(vite@7.0.4(@types/node@25.9.1)(jiti@2.7.0)(lightningcss@1.32.0)): + vitest@4.1.8(@opentelemetry/api@1.9.1)(@types/node@25.9.1)(@vitest/coverage-v8@4.1.8)(@vitest/ui@4.1.8)(vite@7.0.4(@types/node@25.9.1)(jiti@2.7.0)(lightningcss@1.32.0)): dependencies: '@vitest/expect': 4.1.8 '@vitest/mocker': 4.1.8(vite@7.0.4(@types/node@25.9.1)(jiti@2.7.0)(lightningcss@1.32.0)) @@ -10492,6 +10697,7 @@ snapshots: vite: 7.0.4(@types/node@25.9.1)(jiti@2.7.0)(lightningcss@1.32.0) why-is-node-running: 2.3.0 optionalDependencies: + '@opentelemetry/api': 1.9.1 '@types/node': 25.9.1 '@vitest/coverage-v8': 4.1.8(vitest@4.1.8) '@vitest/ui': 4.1.8(vitest@4.1.8) diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index c9d7ac012..88c1487de 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -5,7 +5,7 @@ packages: onlyBuiltDependencies: - '@nestjs/core' - - '@sentry-internal/node-cpu-profiler' + - '@sentry/node-cpu-profiler' - '@scarf/scarf' - '@swc/core' - esbuild