Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,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:^",
Expand Down
16 changes: 11 additions & 5 deletions apps/api/src/hypixel/hypixel.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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,
})
)
)
);
})
);
}
}
10 changes: 9 additions & 1 deletion apps/api/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { config } from "@statsify/util";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import { mkdir } from "node:fs/promises";
import { nodeProfilingIntegration } from "@sentry/profiling-node";

const __dirname = dirname(fileURLToPath(import.meta.url));

Expand All @@ -31,16 +32,23 @@ 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({
dsn: 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"),
});
}
Expand Down
18 changes: 17 additions & 1 deletion apps/api/src/redis/redis.utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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<unknown>).finally(() => span?.finish());
}) as Redis["sendCommand"];

return redis;
}
1 change: 1 addition & 0 deletions apps/discord-bot/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
},
"dependencies": {
"@sentry/node": "^7.118.0",
"@sentry/profiling-node": "^7.120.4",
"@statsify/api-client": "workspace:^",
"@statsify/assets": "workspace:^",
"@statsify/discord": "workspace:^",
Expand Down
1 change: 1 addition & 0 deletions apps/discord-bot/src/commands/base.hypixel-command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ export interface BaseHypixelCommand<T extends GamesWithBackgrounds, K = never> {
description: "",
args: [PlayerArgument],
cooldown: 10,
group: "hypixel",
})
export abstract class BaseHypixelCommand<T extends GamesWithBackgrounds, K = never> {
protected readonly apiService: ApiService;
Expand Down
27 changes: 21 additions & 6 deletions apps/discord-bot/src/commands/duels/duels.command.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,19 +26,34 @@ interface PreProfileData {
modeIcons: DuelsModeIcons;
}

let modeIcons: Promise<DuelsModeIcons> | undefined;

const getModeIcons = async () => {
if (modeIcons) return modeIcons;

modeIcons = readdir(getAssetPath("duels"))
.then((modeIconPaths) =>
Promise.all(
modeIconPaths.map(async (mode) => [mode.replace(".png", ""), await loadImage(getAssetPath(`duels/${mode}`))])
)
)
.then((icons) => Object.fromEntries(icons))
.catch((error) => {
modeIcons = undefined;
throw error;
});

return modeIcons;
};

@Command({ description: (t) => t("commands.duels") })
export class DuelsCommand extends BaseHypixelCommand<DuelsModes, PreProfileData> {
public constructor() {
super(DUELS_MODES);
}

public async getPreProfileData(): Promise<PreProfileData> {
const modeIconPaths = await readdir(getAssetPath("duels"));
const modeIcons = await Promise.all(
modeIconPaths.map(async (mode) => [mode.replace(".png", ""), await loadImage(getAssetPath(`duels/${mode}`))])
);

return { modeIcons: Object.fromEntries(modeIcons) };
return { modeIcons: await getModeIcons() };
}

public getModeEmojis(modes: GameModeWithSubModes<DuelsModes>[]): ModeEmoji[] {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -241,7 +241,7 @@ export class BaseLeaderboardCommand {
cache.clear();
}, 300_000);

currentPage = page || currentPage;
currentPage = page ?? currentPage;

return { ...message, components: [row] };
}
Expand Down Expand Up @@ -271,7 +271,7 @@ export class BaseLeaderboardCommand {
getLeaderboardDataIcon
);

if (params.type === LeaderboardQuery.PAGE && page) cache.set(page, message);
if (params.type === LeaderboardQuery.PAGE && page !== null) cache.set(page, message);

return [message, page];
}
Expand Down
18 changes: 16 additions & 2 deletions apps/discord-bot/src/commands/ratios/ratios.command.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
* https://github.com/Statsify/statsify/blob/main/LICENSE
*/

import * as Sentry from "@sentry/node";
import {
ARCADE_MODES,
ARENA_BRAWL_MODES,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -235,7 +236,20 @@ export class RatiosCommand {
};

const canvas = render(<RatiosProfile {...props} />, 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" }],
Expand Down
14 changes: 12 additions & 2 deletions apps/discord-bot/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { VerifyCommand } from "#commands/verify.command";
import { config } from "@statsify/util";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import { nodeProfilingIntegration } from "@sentry/profiling-node";

const __dirname = dirname(fileURLToPath(import.meta.url));

Expand All @@ -27,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"),
});
}
Expand Down
21 changes: 20 additions & 1 deletion apps/discord-bot/src/lib/command.listener.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
});
Expand Down
41 changes: 28 additions & 13 deletions packages/api-client/src/api.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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<any>;

try {
res = await this.axios.request({
url,
method,
params,
headers,
data: body,
responseType,
});

child?.setHttpStatus(res.status);
} finally {
child?.finish();
}

const data = res.data;

Expand Down
19 changes: 17 additions & 2 deletions packages/assets/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,22 @@ const checkAsset = (file: string) =>

export const getAssetPath = (path: string) => join(PATH, checkAsset(path), path);

const getImage = (path: string) => loadImage(getAssetPath(path));
const images = new Map<string, Promise<Image>>();

const getCachedImage = (path: string) => {
const cachedImage = images.get(path);
if (cachedImage) return cachedImage;

const image = loadImage(path).catch((error) => {
images.delete(path);
throw error;
});

images.set(path, image);
return image;
};

const getImage = (path: string) => getCachedImage(getAssetPath(path));

/**
*
Expand Down Expand Up @@ -73,7 +88,7 @@ export function getLogo(
userOrLogoOrPath: User | UserLogo | string | null,
size?: number
): Promise<Image> {
return loadImage(getLogoPath(userOrLogoOrPath as User, size));
return getCachedImage(getLogoPath(userOrLogoOrPath as User, size));
}

export function getLogoPath(
Expand Down
Loading
Loading