Skip to content
Open
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
2 changes: 1 addition & 1 deletion .gitmodules
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@
url = https://github.com/Statsify/public-assets
[submodule "assets/private"]
path = assets/private
url = https://github.com/Statsify/assets
url = https://github.com/Statsify/assets
3 changes: 2 additions & 1 deletion apps/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@
"@nestjs/core": "^11.1.6",
"@nestjs/platform-fastify": "^11.1.6",
"@nestjs/swagger": "^11.2.0",
"@sentry/node": "^7.118.0",
"@sentry/node": "^10.59.0",
"@sentry/profiling-node": "^10.59.0",
"@statsify/api-client": "workspace:^",
"@statsify/assets": "workspace:^",
"@statsify/logger": "workspace:^",
Expand Down
36 changes: 20 additions & 16 deletions apps/api/src/hypixel/hypixel.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,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()
Expand Down Expand Up @@ -142,27 +142,31 @@ export class HypixelService {
}

private request<T>(url: string, params?: Record<string, unknown>): Observable<T> {
const transaction = Sentry.getCurrentHub().getScope()?.getTransaction();

const child = transaction?.startChild({
op: "http.client",
const span = startSentrySpan({
op: "hypixel.fetch",
description: `GET ${this.httpService.axiosRef.getUri({ url })}`,
data: {
"hypixel.endpoint": url,
"http.method": "GET",
"http.route": url,
},
});

return this.httpService.get(url, { params }).pipe(
tap((res) => {
child?.setHttpStatus(res.status);
child?.finish();
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(
() =>
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?.end())
);
}
}
34 changes: 25 additions & 9 deletions apps/api/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,10 @@ import handlebars from "handlebars";
import packageJson from "../package.json" with { type: "json" };
import { AppModule } from "./app.module.js";
import { DocumentBuilder, SwaggerModule } from "@nestjs/swagger";
import { FastifyAdapter, NestFastifyApplication } from "@nestjs/platform-fastify";
import {
FastifyAdapter,
NestFastifyApplication,
} from "@nestjs/platform-fastify";
import { Logger } from "@statsify/logger";
import { NestFactory } from "@nestjs/core";
import { SentryInterceptor } from "./sentry/index.js";
Expand All @@ -20,6 +23,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;

Expand All @@ -30,16 +34,24 @@ 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 }),
Sentry.httpIntegration({ spans: false, breadcrumbs: true }),
Sentry.mongoIntegration(),
nodeProfilingIntegration(),
],
normalizeDepth: 3,
tracesSampleRate: await config("sentry.tracesSampleRate"),
enableLogs: true,
tracesSampleRate: sentryTracesSampleRate,
profilesSampleRate: sentryProfilesSampleRate,
environment: await config("environment"),
});
}
Expand All @@ -60,12 +72,16 @@ const adapter = new FastifyAdapter({ bodyLimit: 5e6 });
adapter
.getInstance()
.addContentTypeParser("image/png", { parseAs: "buffer" }, (_, body, done) =>
done(null, body)
done(null, body),
);

const app = await NestFactory.create<NestFastifyApplication>(AppModule, adapter, {
logger: new Logger(),
});
const app = await NestFactory.create<NestFastifyApplication>(
AppModule,
adapter,
{
logger: new Logger(),
},
);

// Validation using `class-validator` and `class-transformer`
app.useGlobalPipes(new ValidationPipe({ transform: true }));
Expand All @@ -78,7 +94,7 @@ const redoc = new DocumentBuilder()
.setTitle("Statsify API")
.setVersion(packageJson.version)
.setDescription(
"# Introduction\nThis is the official Statsify API documentation. [Website](https://statsify.net/) - [GitHub](https://github.com/Statsify/statsify)\n# Authentication\n\n<!-- ReDoc-Inject: <security-definitions> -->"
"# Introduction\nThis is the official Statsify API documentation. [Website](https://statsify.net/) - [GitHub](https://github.com/Statsify/statsify)\n# Authentication\n\n<!-- ReDoc-Inject: <security-definitions> -->",
)
.addSecurity("ApiKey", {
type: "apiKey",
Expand Down
72 changes: 36 additions & 36 deletions apps/api/src/leaderboards/leaderboard.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,14 @@
* 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";
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,
Expand All @@ -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();

Expand All @@ -67,9 +60,13 @@ export abstract class LeaderboardService {
}
}

await pipeline.exec();

child?.finish();
await withSentrySpan({
op: "redis.write",
description: `add ${constructor.name} leaderboards`,
data: {
"leaderboard.type": name,
},
}, () => pipeline.exec());
}

public async getLeaderboard<T>(
Expand Down Expand Up @@ -124,7 +121,8 @@ export abstract class LeaderboardService {
field,
top,
bottom - 1,
sort
sort,
type
);

const additionalFieldMetadata = additionalFields.map((k) =>
Expand Down Expand Up @@ -194,13 +192,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();

Expand All @@ -221,9 +212,14 @@ export abstract class LeaderboardService {
}
}

const responses = await pipeline.exec();

child?.finish();
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();

Expand Down Expand Up @@ -272,23 +268,27 @@ export abstract class LeaderboardService {
field: string,
top: number,
bottom: number,
sort = "DESC"
sort = "DESC",
queryType?: LeaderboardQuery
) {
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"));
const leaderboardField = field;
field = `${name}.${leaderboardField}`;

child?.finish();
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") :
this.redis.zrevrange(field, top, bottom, "WITHSCORES")
);

const response: { id: string; score: number; index: number }[] = [];

Expand Down
49 changes: 29 additions & 20 deletions apps/api/src/sentry/sentry.interceptor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,15 @@ import {
InternalServerErrorException,
NestInterceptor,
} from "@nestjs/common";
import { Observable, catchError, tap } from "rxjs";
import { Observable, catchError, finalize } from "rxjs";
import { URL } from "node:url";
import type { FastifyRequest } from "fastify";

@Injectable()
export class SentryInterceptor implements NestInterceptor {
public intercept(
context: ExecutionContext,
next: CallHandler<any>
next: CallHandler<any>,
): Observable<any> | Promise<Observable<any>> {
const req = context.switchToHttp().getRequest<FastifyRequest>();

Expand All @@ -36,34 +36,43 @@ export class SentryInterceptor implements NestInterceptor {
headers: req.headers,
});

let transaction: ReturnType<Sentry.Hub["startTransaction"]> | 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,
attributes: {
"http.method": req.method,
"http.route": url.pathname,
},
});

Sentry.configureScope((scope) => scope.setSpan(transaction));
}

return next.handle().pipe(
catchError((err) => {
const isHttpException = err instanceof HttpException;
const isInternalError = err instanceof InternalServerErrorException;
return new Observable((subscriber) =>
Sentry.withActiveSpan(transaction ?? null, () =>
next
.handle()
.pipe(
catchError((err) => {
const isHttpException = err instanceof HttpException;
const isInternalError =
err instanceof InternalServerErrorException;

if (isHttpException && !isInternalError) {
transaction?.finish();
throw err;
}
if (isHttpException && !isInternalError) {
throw err;
}

Sentry.captureException(err);
transaction?.setHttpStatus(500);
transaction?.finish();
Sentry.captureException(err);
if (transaction) Sentry.setHttpStatus(transaction, 500);

throw err;
}),
tap(() => transaction?.finish())
throw err;
}),
finalize(() => transaction?.end()),
)
.subscribe(subscriber),
),
);
}
}
3 changes: 2 additions & 1 deletion apps/discord-bot/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@
"lint:ci": "oxlint --format=github"
},
"dependencies": {
"@sentry/node": "^7.118.0",
"@sentry/node": "^10.59.0",
"@sentry/profiling-node": "^10.59.0",
"@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
2 changes: 1 addition & 1 deletion apps/discord-bot/src/commands/ratios/ratios.command.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,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
Loading
Loading