Skip to content

Commit

Permalink
feat(error): add sentry reporter integrated with otl
Browse files Browse the repository at this point in the history
refs #247
  • Loading branch information
ygrishajev committed Aug 8, 2024
1 parent 8372f38 commit 0378ee4
Show file tree
Hide file tree
Showing 7 changed files with 173 additions and 34 deletions.
36 changes: 8 additions & 28 deletions apps/api/src/app.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
import "reflect-metadata";
import "@src/core/providers/sentry.provider";

import { serve } from "@hono/node-server";
// TODO: find out how to properly import this
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
import { sentry } from "@hono/sentry";
import * as Sentry from "@sentry/node";
import { Hono } from "hono";
import { cors } from "hono/cors";
import { container } from "tsyringe";

import { AuthInterceptor } from "@src/auth/services/auth.interceptor";
import { config } from "@src/core/config";
import { getSentry, sentryOptions } from "@src/core/providers/sentry.provider";
import { HonoErrorHandlerService } from "@src/core/services/hono-error-handler/hono-error-handler.service";
import { HttpLoggerService } from "@src/core/services/http-logger/http-logger.service";
import { LoggerService } from "@src/core/services/logger/logger.service";
Expand All @@ -23,7 +25,6 @@ import { internalRouter } from "./routers/internalRouter";
import { legacyRouter } from "./routers/legacyRouter";
import { userRouter } from "./routers/userRouter";
import { web3IndexRouter } from "./routers/web3indexRouter";
import { isProd } from "./utils/constants";
import { env } from "./utils/env";
import { bytesToHumanReadableSize } from "./utils/files";
import { Scheduler } from "./scheduler";
Expand All @@ -38,28 +39,11 @@ appHono.use(

const { PORT = 3080, BILLING_ENABLED } = process.env;

Sentry.init({
dsn: env.SentryDSN,
environment: env.NODE_ENV,
serverName: env.SentryServerName,
release: packageJson.version,
enabled: isProd,
integrations: [
// enable HTTP calls tracing
new Sentry.Integrations.Http({ tracing: true })
],

// Set tracesSampleRate to 1.0 to capture 100%
// of transactions for performance monitoring.
// We recommend adjusting this value in production
tracesSampleRate: 0.01
});

const scheduler = new Scheduler({
healthchecksEnabled: env.HealthchecksEnabled === "true",
errorHandler: (task, error) => {
console.error(`Task "${task.name}" failed: ${error}`);
Sentry.captureException(error);
getSentry().captureException(error);
}
});

Expand All @@ -69,15 +53,11 @@ appHono.use(container.resolve(AuthInterceptor).intercept());
appHono.use(
"*",
sentry({
dsn: env.SentryDSN,
environment: env.NODE_ENV,
...sentryOptions,
beforeSend: event => {
event.server_name = env.SentryServerName;
event.server_name = config.SENTRY_SERVER_NAME;
return event;
},
tracesSampleRate: 0.01,
release: packageJson.version,
enabled: isProd
}
})
);

Expand Down Expand Up @@ -140,7 +120,7 @@ export async function initApp() {
});
} catch (error) {
appLogger.error({ event: "APP_INIT_ERROR", error });
Sentry.captureException(error);
getSentry().captureException(error);
}
}

Expand Down
7 changes: 6 additions & 1 deletion apps/api/src/core/config/env.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,12 @@ const envSchema = z.object({
// TODO: make required once billing is in prod
POSTGRES_DB_URI: z.string().optional(),
POSTGRES_MAX_CONNECTIONS: z.number({ coerce: true }).optional().default(20),
DRIZZLE_MIGRATIONS_FOLDER: z.string().optional().default("./drizzle")
DRIZZLE_MIGRATIONS_FOLDER: z.string().optional().default("./drizzle"),
DEPLOYMENT_ENV: z.string().optional().default("production"),
SENTRY_TRACES_RATE: z.number({ coerce: true }).optional().default(0.01),
SENTRY_ENABLED: z.enum(["true", "false"]).optional().default("false"),
SENTRY_SERVER_NAME: z.string().optional(),
SENTRY_DSN: z.string().optional()
});

export const envConfig = envSchema.parse(process.env);
27 changes: 27 additions & 0 deletions apps/api/src/core/providers/sentry.provider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import * as GlobalSentry from "@sentry/node";
import { container, inject } from "tsyringe";

import { config } from "@src/core/config";
import packageJson from "../../../package.json";

export const sentryOptions = {
dsn: config.SENTRY_DSN,
environment: config.DEPLOYMENT_ENV || config.NODE_ENV,
tracesSampleRate: config.SENTRY_TRACES_RATE,
release: packageJson.version,
enabled: config.SENTRY_ENABLED === "true"
};

GlobalSentry.init({
...sentryOptions,
serverName: config.SENTRY_SERVER_NAME,
integrations: [new GlobalSentry.Integrations.Http({ tracing: true })]
});

const SENTRY = "SENTRY";

container.register(SENTRY, { useValue: GlobalSentry });

export type Sentry = typeof GlobalSentry;
export const InjectSentry = () => inject(SENTRY);
export const getSentry = () => container.resolve<Sentry>(SENTRY);
Original file line number Diff line number Diff line change
@@ -1,19 +1,27 @@
import { context, trace } from "@opentelemetry/api";
import type { Event } from "@sentry/types";
import type { Context, Env } from "hono";
import { isHttpError } from "http-errors";
import omit from "lodash/omit";
import { singleton } from "tsyringe";
import { ZodError } from "zod";

import { InjectSentry, Sentry } from "@src/core/providers/sentry.provider";
import { LoggerService } from "@src/core/services/logger/logger.service";
import { SentryEventService } from "@src/core/services/sentry-event/sentry-event.service";

@singleton()
export class HonoErrorHandlerService {
private readonly logger = new LoggerService({ context: "ErrorHandler" });

constructor() {
constructor(
@InjectSentry() private readonly sentry: Sentry,
private readonly sentryEventService: SentryEventService
) {
this.handle = this.handle.bind(this);
}

handle<E extends Env = any>(error: Error, c: Context<E>): Response | Promise<Response> {
async handle<E extends Env = any>(error: Error, c: Context<E>): Promise<Response> {

Check warning on line 24 in apps/api/src/core/services/hono-error-handler/hono-error-handler.service.ts

View workflow job for this annotation

GitHub Actions / validate-n-build

Unexpected any. Specify a different type
this.logger.error(error);

if (isHttpError(error)) {
Expand All @@ -25,6 +33,36 @@ export class HonoErrorHandlerService {
return c.json({ error: "BadRequestError", data: error.errors }, { status: 400 });
}

await this.reportError(error, c);

return c.json({ error: "InternalServerError" }, { status: 500 });
}

private async reportError<E extends Env = any>(error: Error, c: Context<E>): Promise<void> {

Check warning on line 41 in apps/api/src/core/services/hono-error-handler/hono-error-handler.service.ts

View workflow job for this annotation

GitHub Actions / validate-n-build

Unexpected any. Specify a different type
const id = this.sentry.captureEvent(await this.getSentryEvent(error, c));
this.logger.info({ event: "SENTRY_EVENT_REPORTED", id });
}

private async getSentryEvent<E extends Env = any>(error: Error, c: Context<E>): Promise<Event> {

Check warning on line 46 in apps/api/src/core/services/hono-error-handler/hono-error-handler.service.ts

View workflow job for this annotation

GitHub Actions / validate-n-build

Unexpected any. Specify a different type
const event = this.sentry.addRequestDataToEvent(this.sentryEventService.toEvent(error), {
method: c.req.method,
url: c.req.url,
headers: omit(Object.fromEntries(c.req.raw.headers), ["x-anonymous-user-id"]),
body: await c.req.json()
});
const currentSpan = trace.getSpan(context.active());

if (currentSpan) {
const context = currentSpan.spanContext();
event.contexts = {
...event.contexts,
trace: {
trace_id: context.traceId,
span_id: context.spanId
}
};
}

return event;
}
}
92 changes: 92 additions & 0 deletions apps/api/src/core/services/sentry-event/sentry-event.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import * as Sentry from "@sentry/node";
import type { Event } from "@sentry/types";
import type { StackFrame } from "@sentry/types/types/stackframe";
import { singleton } from "tsyringe";

type ErrorType = Error & {
status?: number;
statusCode?: number;
toJSON?: () => Record<string, any>;
};
type ObjectErrorType = Record<string, any>;
export type ErrorCatchType = ErrorType | ObjectErrorType | string | unknown;

@singleton()
export class SentryEventService {
public toEvent(error: ErrorCatchType): Event {
const reportableError = error && typeof error === "object" && "toJSON" in error && typeof error.toJSON === "function" ? error.toJSON() : error;

if (reportableError instanceof Error) {
return this.toEventFromError(reportableError);
}

if (typeof reportableError === "string") {
return this.toEventFromMessage(reportableError);
}

if (typeof reportableError === "object" && reportableError !== null) {
return this.toEventFromObject(reportableError);
}

return reportableError;
}

public toEventFromError(error: ErrorType): Event {
return {
message: error.message,
exception: {
values: [
{
type: error.constructor.name,
value: error.message,
stacktrace: this.getStackFrames(error)
}
]
}
};
}

public toEventFromMessage(message: string): Event {
return {
message,
exception: {
values: [
{
type: message.constructor.name,
value: message
}
]
}
};
}

public toEventFromObject(payload: ObjectErrorType): Event {
let message = typeof payload === "object" && (("message" in payload && payload.message) || ("title" in payload && payload.title));

if (!message) {
message = JSON.stringify(payload);

if (message.length > 100) {
message = message.slice(0, 100) + "...";
}
}

return {
message: message,
extra: payload,
exception: {
values: [
{
type: payload.name || payload.constructor.name,
value: message,
stacktrace: this.getStackFrames(payload)
}
]
}
};
}

private getStackFrames(payload?: { stack?: string }): { frames: StackFrame[] } | undefined {
return payload?.stack ? { frames: Sentry.defaultStackParser(payload.stack) } : undefined;
}
}
1 change: 0 additions & 1 deletion apps/api/src/utils/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ export const averageDaysInMonth = 30.437;
export const averageHoursInAMonth = averageDaysInMonth * 24;
export const averageBlockCountInAMonth = (averageDaysInMonth * 24 * 60 * 60) / averageBlockTime;
export const averageBlockCountInAnHour = (60 * 60) / averageBlockTime;
export const isProd = env.NODE_ENV === "production";

export const dataFolderPath = "./data";

Expand Down
2 changes: 0 additions & 2 deletions apps/api/src/utils/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,8 @@ import { z } from "zod";

export const env = z
.object({
SentryDSN: z.string().optional(),
AKASHLYTICS_CORS_WEBSITE_URLS: z.string().optional(),
NODE_ENV: z.string().optional(),
SentryServerName: z.string().optional(),
HealthchecksEnabled: z.string().optional(),
AkashDatabaseCS: z.string().optional(),
AkashTestnetDatabaseCS: z.string().optional(),
Expand Down

0 comments on commit 0378ee4

Please sign in to comment.