Skip to content
Merged
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
10 changes: 4 additions & 6 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,16 +1,14 @@
HMAC_SECRET="YOUR_VERY_SECRET_CODE_GOES_HERE" # For max security, use "openssl rand -base64 64" to generate one
DATABASE_URL="YOUR_NOT_SO_SECRET_DATABASE_URL_GOES_HERE"

# Dodo Payments (replaces LEMON_SQUEEZY_*)
# Sentry
SENTRY_DSN="" # Your Sentry DSN for error monitoring

# Dodo Payments
DODO_PAYMENTS_API_KEY="" # Your Dodo Payments API key (from dashboard → Developer → API)
DODO_PAYMENTS_PRODUCT_ID="" # Your Dodo product ID (prod_xxxxx)
DODO_PAYMENTS_WEBHOOK_SECRET= # Webhook signing secret

# Legacy payment provider (deprecated - use DODO_PAYMENTS_* above)
PAYMENT_PROVIDER_API_KEY=""
PAYMENT_PROVIDER_STORE_ID=""
PAYMENT_PROVIDER_VARIANT_ID=""
PAYMENT_PROVIDER_WEBHOOK_SECRET=

TEST_API_KEY=
REDIS_URL=
Expand Down
216 changes: 216 additions & 0 deletions bun.lock

Large diffs are not rendered by default.

6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"start": "bun run src/server.ts",
"init_key": "bun run src/utils/generateInitialAPIKey.ts",
"format": "bunx prettier --write .",
"typecheck": "bunx tsgo",
"proto:pull": "git submodule update --remote --merge proto",
"proto:push": "cd proto && git add . && git commit -s -m \"$1\" && git push && cd .."
},
Expand All @@ -31,7 +32,8 @@
"typescript": "^5"
},
"dependencies": {

"@opentelemetry/instrumentation-pg": "^0.69.0",
"@sentry/bun": "^10.51.0",
"bullmq": "^5.75.2",
"dodopayments": "^2.30.0",
"drizzle-orm": "^0.44.7",
Expand All @@ -45,4 +47,4 @@
"postgres": "^3.4.7",
"zod": "^4.1.12"
}
}
}
7 changes: 7 additions & 0 deletions src/errors/logger.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import pino, { type Logger as PinoLogger } from "pino";
import * as Sentry from "@sentry/bun";

/**
* Wide Event interface for structured logging.
Expand Down Expand Up @@ -117,6 +118,12 @@ class WideEventLogger {
* Log fatal errors that prevent the server from operating.
*/
fatal(message: string, error?: Error): void {
if (error) {
Sentry.captureException(error, { level: "fatal" });
} else {
Sentry.captureMessage(message, { level: "fatal" });
}

this.pino.fatal(
{
error: error
Expand Down
17 changes: 17 additions & 0 deletions src/interceptors/logging.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { status as grpcStatus } from "@grpc/grpc-js";
import type { sendUnaryData, ServerErrorResponse } from "@grpc/grpc-js";
import * as Sentry from "@sentry/bun";
import { logger } from "../errors/logger";
import {
wideEventContextKey,
Expand All @@ -25,13 +26,24 @@ export function loggingInterceptor(
// Attach builder to call object
call[wideEventContextKey] = builder;

Sentry.addBreadcrumb({
category: "request",
message: `gRPC: ${url}`,
data: { requestId, method },
level: "info",
});

// Wrap callback to capture errors
const originalCallback = callback;
const wrappedCallback: sendUnaryData<unknown> = (error, response, trailer, flags) => {
if (error) {
const errorDetails = extractErrorDetails(error);
const statusCode = grpcStatusToHttpStatus(errorDetails.code);

Sentry.captureException(error, {
extra: { requestId, method: url, statusCode },
});

builder.setError(statusCode, {
type: errorDetails.type,
message: errorDetails.message,
Expand All @@ -58,6 +70,11 @@ export function loggingInterceptor(
if (!builder['event'].outcome) {
const errorDetails = extractErrorDetails(error);
const statusCode = grpcStatusToHttpStatus(errorDetails.code);

Sentry.captureException(error, {
extra: { requestId, method: url, statusCode },
});

builder.setError(statusCode, {
type: errorDetails.type,
message: errorDetails.message,
Expand Down
8 changes: 7 additions & 1 deletion src/routes/gRPC/events/streamEvents.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { ServerReadableStream, sendUnaryData } from "@grpc/grpc-js";
import * as Sentry from "@sentry/bun";
import {
StreamEventRequest,
StreamEventResponse,
Expand Down Expand Up @@ -38,7 +39,12 @@ export async function streamEvents(
await storeEvent(event, apiKeyId);
eventsProcessed++;
} catch (innerError) {
console.log(innerError);
Sentry.addBreadcrumb({
category: "streamEvents",
message: `Event processing failed: ${innerError instanceof Error ? innerError.message : String(innerError)}`,
level: "error",
});
Sentry.captureException(innerError);
callback(innerError as Error, null);
return;
}
Expand Down
5 changes: 5 additions & 0 deletions src/routes/http/api/onboarding.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { FastifyRequest, FastifyReply } from "fastify";
import * as Sentry from "@sentry/bun";
import { ZodError } from "zod";
import { onboardingCronSchema } from "../../../zod/internals.ts";
import { addOnboardingCronJob } from "../../../queues/onboarding.ts";
Expand Down Expand Up @@ -53,6 +54,10 @@ export async function handleOnboarding(
reply.code(201);
return { crons };
} catch (error) {
Sentry.captureException(error, {
extra: { context: "onboarding route handler" },
});

if (error instanceof ZodError) {
const issues = error.issues
.map((issue) => `${issue.path.join(".")}: ${issue.message}`)
Expand Down
16 changes: 15 additions & 1 deletion src/routes/http/createdCheckout.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import DodoPayments from "dodopayments";
import * as Sentry from "@sentry/bun";
import { Payment } from "../../events/RawEvents/Payment.ts";
import { StorageAdapterFactory } from "../../factory/EventStorageAdapterFactory.ts";
import type { WideEventBuilder } from "../../context/requestContext.ts";
Expand Down Expand Up @@ -55,7 +56,10 @@ export async function handleDodoWebhook(
webhookPayload = client.webhooks.unwrap(rawBody, {
headers,
}) as unknown as DodoWebhookPayload;
} catch {
} catch (error) {
Sentry.captureException(error, {
extra: { context: "webhook signature verification" },
});
builder.setError(401, {
type: "AuthenticationError",
message: "Invalid webhook signature",
Expand Down Expand Up @@ -186,6 +190,13 @@ export async function handleDodoWebhook(
body: { message: "Webhook processed successfully" },
};
} catch (dbError) {
Sentry.captureException(dbError, {
extra: {
context: "payment event storage",
checkoutSessionId: checkout_session_id,
paymentId: payment_id,
},
});
const errorMessage =
dbError instanceof Error ? dbError.message : String(dbError);
builder.setError(500, {
Expand All @@ -197,6 +208,9 @@ export async function handleDodoWebhook(
return { statusCode: 500, body: { error: "Database error" } };
}
} catch (error) {
Sentry.captureException(error, {
extra: { context: "unexpected webhook error" },
});
const errorMessage = error instanceof Error ? error.message : String(error);
builder.setError(500, {
type: "InternalError",
Expand Down
4 changes: 4 additions & 0 deletions src/routes/http/registerWebhookRoutes.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { FastifyInstance, FastifyRequest, FastifyReply } from "fastify";
import * as Sentry from "@sentry/bun";
import { createWideEventBuilder, generateRequestId } from "../../context/requestContext.ts";
import { logger } from "../../errors/logger.ts";
import { handleDodoWebhook } from "./createdCheckout.ts";
Expand Down Expand Up @@ -61,6 +62,9 @@ export async function registerWebhookRoutes(
reply.code(result.statusCode);
return result.body;
} catch (error) {
Sentry.captureException(error, {
extra: { context: "webhook route handler" },
});
const err = error instanceof Error ? error : new Error(String(error));
builder.setError(500, {
type: "InternalError",
Expand Down
28 changes: 28 additions & 0 deletions src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,29 @@ import { startRawGrpcServer } from "./servers/rawGrpcServer.ts";
import { startFastifyServer } from "./servers/fastifyServer.ts";
import { OnboardingWorker } from "./workers/onboarding.ts";
import { getRedisConnection } from "./storage/db/redis.ts";
import * as Sentry from "@sentry/bun";

const isProduction = process.env.NODE_ENV === "production";

Sentry.init({
dsn: process.env.SENTRY_DSN,
environment: isProduction ? "production" : "development",
release: process.env.GIT_COMMIT_SHA ?? "dev",
integrations: [Sentry.fastifyIntegration(), Sentry.httpIntegration()],
tracesSampleRate: isProduction ? 0.1 : 1.0,
ignoreErrors: ["ConnectionRefusedError", "ECONNREFUSED"],
maxBreadcrumbs: 10,
});

process.on("uncaughtException", (error) => {
Sentry.captureException(error);
Sentry.flush().then(() => process.exit(1));
});

process.on("unhandledRejection", (reason) => {
Sentry.captureException(reason);
Sentry.flush().then(() => process.exit(1));
});

const DATABASE_URL = process.env.DATABASE_URL;
const HMAC_SECRET = process.env.HMAC_SECRET;
Expand All @@ -24,6 +47,10 @@ if (!REDIS_URL) {
throw new Error("REDIS_URL environmentvariable is not set");
}

if (!process.env.SENTRY_DSN) {
logger.fatal("SENTRY_DSN environment variable is not set — errors will NOT be reported to Sentry");
}

getPostgresDB(DATABASE_URL);
getRedisConnection(REDIS_URL);

Expand All @@ -44,6 +71,7 @@ process.on("beforeExit", async () => {
if (onboardingWorker) {
await onboardingWorker.close();
}
await Sentry.flush(2000);
});

void main();
6 changes: 3 additions & 3 deletions src/zod/event.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ const SDKCallDataSchema: z.ZodType<SDKCallEventData> = z
} else if (v.expr) {
debitAmount = await parseAndEvaluateExpr(v.expr);
} else {
debitAmount = Math.floor(v.amount * 100);
debitAmount = v.amount;
}
return { sdkCallType: v.sdkcalltype, debitAmount };
});
Expand All @@ -62,7 +62,7 @@ const AITokenUsageDataSchema: z.ZodType<AITokenUsageEventData> = z
} else if (v.inputexpr) {
inputDebitAmount = await parseAndEvaluateExpr(v.inputexpr);
} else {
inputDebitAmount = Math.floor(v.inputamount * 100);
inputDebitAmount = v.inputamount;
}

let outputDebitAmount: number;
Expand All @@ -74,7 +74,7 @@ const AITokenUsageDataSchema: z.ZodType<AITokenUsageEventData> = z
} else if (v.outputexpr) {
outputDebitAmount = await parseAndEvaluateExpr(v.outputexpr);
} else {
outputDebitAmount = Math.floor(v.outputamount * 100);
outputDebitAmount = v.outputamount;
}

return {
Expand Down
Loading