Skip to content

Commit

Permalink
api: integrate sentry monitoring (#43)
Browse files Browse the repository at this point in the history
* integrate sentry api observability

* update error handling

* parse error message from thirdweb engine

* add fetch depth to release step

* add build steps to release

* add sentry vars to terraform

* fix sentry_environment vals

* fix sentry var nesting

* fix dev env folder in readme
  • Loading branch information
alecananian authored Jun 8, 2024
1 parent 786c1c7 commit 003350e
Show file tree
Hide file tree
Showing 24 changed files with 1,300 additions and 438 deletions.
31 changes: 31 additions & 0 deletions .github/workflows/deploy-api.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,41 @@ jobs:
AWS_REGION=${{ vars.AWS_REGION }}
API_ENV_SECRET_NAME=${{ vars.API_ENV_SECRET_NAME }}
DATABASE_SECRET_NAME=${{ vars.DATABASE_SECRET_NAME }}
SENTRY_DSN=${{ vars.SENTRY_DSN }}
SENTRY_ENVIRONMENT=${{ vars.ENVIRONMENT }}
- name: Deploy Amazon ECS task definition
uses: aws-actions/amazon-ecs-deploy-task-definition@v1
with:
task-definition: ${{ steps.task-def.outputs.task-definition }}
service: ${{ vars.ECS_SERVICE }}
cluster: ${{ vars.ECS_CLUSTER }}
wait-for-service-stability: true
release:
needs: deploy
runs-on: ubuntu-latest
environment: ${{ startsWith(github.ref, 'refs/tags/') && 'production' || 'development' }}
steps:
- name: Check out repo
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: 20
- name: Install dependencies
run: npm ci
- name: Build packages
run: npm run build:core
- name: Build code
run: npm run build:api
- name: Create Sentry release
uses: getsentry/action-release@v1
env:
SENTRY_ORG: spellcaster
SENTRY_PROJECT: tdk-api
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
with:
environment: ${{ vars.ENVIRONMENT }}
working_directory: ./apps/api
sourcemaps: ./dist
2 changes: 2 additions & 0 deletions apps/api/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,5 @@ THIRDWEB_SECRET_KEY=
TROVE_API_URL=https://trove-api-dev.treasure.lol
TROVE_API_KEY=
ZEEVERSE_API_URL=https://api.zee-verse.com
SENTRY_DSN=
SENTRY_ENVIRONMENT=development
2 changes: 2 additions & 0 deletions apps/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
"@fastify/swagger-ui": "^3.0.0",
"@fastify/type-provider-typebox": "^4.0.0",
"@prisma/client": "^5.3.0",
"@sentry/node": "^8.7.0",
"@sentry/profiling-node": "^8.7.0",
"@sinclair/typebox": "^0.32.5",
"@thirdweb-dev/auth": "^4.1.4",
"@thirdweb-dev/engine": "^0.0.9",
Expand Down
4 changes: 4 additions & 0 deletions apps/api/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import "./instrument";

import type { TypeBoxTypeProvider } from "@fastify/type-provider-typebox";
import { PrismaClient } from "@prisma/client";
import * as Sentry from "@sentry/node";
import { Engine } from "@thirdweb-dev/engine";
import type { SupportedChainId } from "@treasure-dev/tdk-core";
import { SUPPORTED_CHAINS } from "@treasure-dev/tdk-core";
Expand Down Expand Up @@ -74,6 +77,7 @@ const main = async () => {
};

// Middleware
Sentry.setupFastifyErrorHandler(app);
await withSwagger(app);
await withCors(app);
await withErrorHandler(app);
Expand Down
10 changes: 10 additions & 0 deletions apps/api/src/instrument.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import * as Sentry from "@sentry/node";
import { nodeProfilingIntegration } from "@sentry/profiling-node";

Sentry.init({
dsn: process.env.SENTRY_DSN,
environment: process.env.SENTRY_ENVIRONMENT || "production",
integrations: [nodeProfilingIntegration(), Sentry.prismaIntegration()],
tracesSampleRate: 1.0,
profilesSampleRate: 1.0,
});
8 changes: 8 additions & 0 deletions apps/api/src/middleware/auth.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { Prisma } from "@prisma/client";
import * as Sentry from "@sentry/node";
import { PrivateKeyWallet } from "@thirdweb-dev/auth/evm";
import { ThirdwebAuth } from "@thirdweb-dev/auth/fastify";
import type { AddressString } from "@treasure-dev/tdk-core";
Expand Down Expand Up @@ -126,6 +127,10 @@ export const withAuth = async (
const authResult = await verifyAuth(auth, req);
if (authResult.valid) {
req.userAddress = authResult.parsedJWT.sub as AddressString;
Sentry.setUser({
id: (authResult.parsedJWT.ctx as { id: string } | undefined)?.id,
username: req.userAddress,
});
} else {
req.authError = authResult.error;
}
Expand All @@ -134,5 +139,8 @@ export const withAuth = async (
req.overrideUserAddress = req.headers["x-account-address"]?.toString() as
| AddressString
| undefined;
if (req.overrideUserAddress) {
Sentry.setUser({ username: req.overrideUserAddress });
}
});
};
3 changes: 3 additions & 0 deletions apps/api/src/middleware/chain.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import * as Sentry from "@sentry/node";
import {
DEFAULT_TDK_CHAIN_ID,
SUPPORTED_CHAIN_IDS,
Expand All @@ -24,5 +25,7 @@ export const withChain = async (app: FastifyInstance) => {
chainId && (SUPPORTED_CHAIN_IDS as number[]).includes(chainId)
? (chainId as SupportedChainId)
: DEFAULT_TDK_CHAIN_ID;

Sentry.setContext("chain", { chainId: req.chainId });
});
};
5 changes: 2 additions & 3 deletions apps/api/src/middleware/error.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import type { FastifyInstance } from "fastify";

export const withErrorHandler = async (app: FastifyInstance) =>
app.setErrorHandler((err, req, reply) => {
console.error(`Error occurred in ${req.routerPath}:`, err);
reply.code(err.statusCode ?? 500).send({ error: err.message });
app.setErrorHandler((err, _, reply) => {
reply.code(err.statusCode ?? 500).send({ ...err, error: err.message });
});
5 changes: 5 additions & 0 deletions apps/api/src/middleware/project.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import * as Sentry from "@sentry/node";
import type { FastifyInstance } from "fastify";

import type { TdkApiContext } from "../types";
Expand Down Expand Up @@ -40,6 +41,10 @@ export const withProject = async (
if (project.backendWallets.length > 0) {
req.backendWallet = project.backendWallets[0].address;
}

Sentry.setContext("project", {
slug: projectId,
});
}
}

Expand Down
13 changes: 11 additions & 2 deletions apps/api/src/routes/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {
} from "../schema";
import type { TdkApiContext } from "../types";
import { fetchEmbeddedWalletUser } from "../utils/embeddedWalletApi";
import { TdkError } from "../utils/error";
import { logInWithZeeverse, verifyZeeverseToken } from "../utils/zeeverse";

export const authRoutes =
Expand Down Expand Up @@ -182,7 +183,11 @@ export const authRoutes =
}

if (!token) {
return reply.code(401).send({ error: "Unauthorized" });
throw new TdkError({
code: "TDK_UNAUTHORIZED",
message: "Unauthorized",
data: { projectId },
});
}

reply.send({
Expand Down Expand Up @@ -228,7 +233,11 @@ export const authRoutes =
}

if (!result.userId || !result.email) {
return reply.code(401).send({ error: "Unauthorized" });
throw new TdkError({
code: "TDK_UNAUTHORIZED",
message: "Unauthorized",
data: { projectId },
});
}

return reply.send(result);
Expand Down
7 changes: 6 additions & 1 deletion apps/api/src/routes/harvesters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
readHarvesterReplySchema,
} from "../schema";
import type { TdkApiContext } from "../types";
import { TdkError } from "../utils/error";

export const harvestersRoutes =
({ env, wagmiConfig }: TdkApiContext): FastifyPluginAsync =>
Expand Down Expand Up @@ -57,7 +58,11 @@ export const harvestersRoutes =
});

if (harvesterInfo.nftHandlerAddress === zeroAddress) {
return reply.code(404).send({ error: "Not found" });
throw new TdkError({
code: "TDK_NOT_FOUND",
message: "NftHandler not found",
data: { harvesterAddress },
});
}

const userAddress = overrideUserAddress ?? authUserAddress;
Expand Down
11 changes: 10 additions & 1 deletion apps/api/src/routes/projects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
readProjectReplySchema,
} from "../schema";
import type { TdkApiContext } from "../types";
import { TdkError } from "../utils/error";

export const projectsRoutes =
({ env, db }: TdkApiContext): FastifyPluginAsync =>
Expand All @@ -27,7 +28,7 @@ export const projectsRoutes =
},
},
async ({ chainId, params: { slug } }, reply) => {
const project = await db.project.findUniqueOrThrow({
const project = await db.project.findUnique({
where: { slug },
select: {
slug: true,
Expand Down Expand Up @@ -55,6 +56,14 @@ export const projectsRoutes =
},
},
});
if (!project) {
throw new TdkError({
code: "TDK_NOT_FOUND",
message: "Project not found",
data: { slug },
});
}

const backendWallets = project.backendWallets.map(
({ address }) => address,
);
Expand Down
38 changes: 24 additions & 14 deletions apps/api/src/routes/transactions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
readTransactionReplySchema,
} from "../schema";
import type { TdkApiContext } from "../types";
import { TdkError, parseEngineErrorMessage } from "../utils/error";

export const transactionsRoutes =
({ engine }: TdkApiContext): FastifyPluginAsync =>
Expand Down Expand Up @@ -44,11 +45,11 @@ export const transactionsRoutes =
body: postBody,
} = req;
if (!userAddress) {
console.error(
"Error authenticating user for transaction create:",
authError,
);
return reply.code(401).send({ error: `Unauthorized: ${authError}` });
throw new TdkError({
code: "TDK_UNAUTHORIZED",
message: "Unauthorized",
data: { authError },
});
}

const { address, ...body } = postBody;
Expand All @@ -63,10 +64,17 @@ export const transactionsRoutes =
);
reply.send(result);
} catch (err) {
console.error("Contract write error:", err);
if (err instanceof Error) {
reply.code(500).send({ error: err.message });
}
throw new TdkError({
code: "TDK_CREATE_TRANSACTION",
message: `Error creating transaction: ${parseEngineErrorMessage(err) ?? "Unknown error"}`,
data: {
chainId,
backendWallet,
userAddress,
address,
...body,
},
});
}
},
);
Expand All @@ -86,14 +94,16 @@ export const transactionsRoutes =
},
},
async (req, reply) => {
const { queueId } = req.params;
try {
const data = await engine.transaction.status(req.params.queueId);
const data = await engine.transaction.status(queueId);
reply.send(data.result);
} catch (err) {
console.error("Transaction status error:", err);
if (err instanceof Error) {
reply.code(500).send({ error: err.message });
}
throw new TdkError({
code: "TDK_READ_TRANSACTION",
message: `Error fetching transaction: ${parseEngineErrorMessage(err) ?? "Unknown error"}`,
data: { queueId },
});
}
},
);
Expand Down
17 changes: 11 additions & 6 deletions apps/api/src/routes/users.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
readCurrentUserReplySchema,
} from "../schema";
import type { TdkApiContext } from "../types";
import { TdkError } from "../utils/error";

export const usersRoutes =
({ db, wagmiConfig }: TdkApiContext): FastifyPluginAsync =>
Expand All @@ -31,11 +32,11 @@ export const usersRoutes =
async (req, reply) => {
const { chainId, userAddress, authError } = req;
if (!userAddress) {
console.error(
"Error authenticating user for user details read:",
authError,
);
return reply.code(401).send({ error: `Unauthorized: ${authError}` });
throw new TdkError({
code: "TDK_UNAUTHORIZED",
message: "Unauthorized",
data: { authError },
});
}

const [dbUser, allActiveSigners] = await Promise.all([
Expand All @@ -51,7 +52,11 @@ export const usersRoutes =
]);

if (!dbUser) {
return reply.code(401).send({ error: "Unauthorized" });
throw new TdkError({
code: "TDK_NOT_FOUND",
message: "User not found",
data: { userAddress },
});
}

reply.send({
Expand Down
52 changes: 52 additions & 0 deletions apps/api/src/utils/error.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import * as Sentry from "@sentry/node";
import { ApiError } from "@thirdweb-dev/engine";

type ErrorCode =
| "TDK_UNAUTHORIZED"
| "TDK_FORBIDDEN"
| "TDK_NOT_FOUND"
| "TDK_CREATE_TRANSACTION"
| "TDK_READ_TRANSACTION";

const ERROR_STATUS_CODE_MAPPING: Partial<Record<ErrorCode, number>> = {
TDK_UNAUTHORIZED: 401,
TDK_FORBIDDEN: 403,
TDK_NOT_FOUND: 404,
};

export class TdkError extends Error {
code: ErrorCode;
statusCode: number;
data: object | undefined;

constructor({
code,
message,
statusCode,
data,
}: {
code: ErrorCode;
message: string;
statusCode?: number;
data?: object;
}) {
super(message);
this.name = "TdkError";
this.code = code;
this.statusCode = statusCode ?? ERROR_STATUS_CODE_MAPPING[code] ?? 500;
this.data = data;
Sentry.setExtra("error", this);
}
}

export const parseEngineErrorMessage = (err: ApiError | Error) => {
if (err instanceof ApiError && err.body.error?.message) {
return err.body.error.message as string;
}

if (err instanceof Error) {
return err.message;
}

return undefined;
};
Loading

0 comments on commit 003350e

Please sign in to comment.