Skip to content

Commit a01f98b

Browse files
authored
feat: sentry for error tracking (#59)
* successfully caught error on Sentry via tunnel * ok working properly kinda good enough * no regrets changes * working on backend * refactor part 1 * fix: error handling * setup dsn, tweak sample rates * remove debug: true --------- Co-authored-by: zx <[email protected]>
1 parent 993a3b2 commit a01f98b

File tree

21 files changed

+432
-34
lines changed

21 files changed

+432
-34
lines changed

bun.lock

Lines changed: 130 additions & 3 deletions
Large diffs are not rendered by default.

infra/Api.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,24 @@ const hono = new sst.cloudflare.Worker("Hono", {
1010
handler: "./packages/workers/src/api.ts",
1111
link: [auth, authKv, cacheKv, ...allSecrets],
1212
domain: "api." + domain,
13+
// eventually: enable sourcemaps when this is fixed: https://github.com/sst/sst/issues/4514
14+
// build: {
15+
// esbuild: {
16+
// sourcemap: true,
17+
// plugins: [
18+
// // Put the Sentry esbuild plugin after all other plugins
19+
// sentryEsbuildPlugin({
20+
// authToken: process.env.SENTRY_AUTH_TOKEN,
21+
// org: "coder-aw",
22+
// project: "node-cloudflare-workers",
23+
// }),
24+
// ],
25+
// },
26+
// },
1327
transform: {
1428
worker: {
29+
compatibilityDate: "2024-09-23",
30+
compatibilityFlags: ["nodejs_compat"],
1531
// staging will bind to dev wrangler workers too
1632
serviceBindings: [
1733
{

infra/Dns.ts

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,18 @@
1+
import type { DomainStages } from "./types";
2+
3+
export const STAGES = {
4+
PROD: "prod",
5+
STG: "stg",
6+
UAT: "uat",
7+
} as const satisfies DomainStages;
8+
9+
const BASE_DOMAIN = "semhub.dev";
10+
111
export const domain =
212
{
3-
prod: "semhub.dev",
4-
stg: "stg.semhub.dev",
5-
uat: "uat.semhub.dev",
13+
[STAGES.PROD]: BASE_DOMAIN,
14+
[STAGES.STG]: `stg.${BASE_DOMAIN}`,
15+
[STAGES.UAT]: `uat.${BASE_DOMAIN}`,
616
}[$app.stage] || $app.stage + ".stg.semhub.dev";
717

818
// export const zone = cloudflare.getZoneOutput({

infra/Secret.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ export const secret = {
2323
githubAppClientSecret: new sst.Secret("SEMHUB_GITHUB_APP_CLIENT_SECRET"),
2424
githubAppId: new sst.Secret("SEMHUB_GITHUB_APP_ID"),
2525
githubAppPrivateKey: new sst.Secret("SEMHUB_GITHUB_APP_PRIVATE_KEY"),
26+
sentryAuthToken: new sst.Secret("SENTRY_AUTH_TOKEN"),
2627
keys,
2728
};
2829

infra/Web.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
import { apiUrl } from "./Api";
22
import { domain } from "./Dns";
3+
import { secret } from "./Secret";
34

45
const web = new sst.aws.StaticSite("Web", {
56
path: "packages/web",
67
environment: {
78
// when adding new env vars, you may have to rm -rf node_modules
9+
SENTRY_AUTH_TOKEN: secret.sentryAuthToken.value,
10+
SST_STAGE: $app.stage,
811
VITE_SST_STAGE: $app.stage,
912
VITE_API_URL: apiUrl.apply((url) => {
1013
if (typeof url !== "string") {

infra/types.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,9 @@ export type CronPatterns = {
33
readonly SYNC_ISSUE: "*/20 * * * *";
44
readonly SYNC_EMBEDDING: "0 * * * *";
55
};
6+
7+
export type DomainStages = {
8+
readonly PROD: "prod";
9+
readonly STG: "stg";
10+
readonly UAT: "uat";
11+
};
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import type { DomainStages } from "@/infra/types";
2+
3+
export const STAGES = {
4+
PROD: "prod",
5+
STG: "stg",
6+
UAT: "uat",
7+
} as const satisfies DomainStages;
8+
9+
export const APP_DOMAIN = "semhub.dev";
10+
export const LOCAL_DEV_DOMAIN = `local.${APP_DOMAIN}`;
11+
export const APP_STG_DOMAIN = `stg.${APP_DOMAIN}`;
12+
export const APP_UAT_DOMAIN = `uat.${APP_DOMAIN}`;

packages/core/sst-env.d.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,10 @@ declare module "sst" {
5252
"type": "sst.sst.Secret"
5353
"value": string
5454
}
55+
"SENTRY_AUTH_TOKEN": {
56+
"type": "sst.sst.Secret"
57+
"value": string
58+
}
5559
"Web": {
5660
"type": "sst.aws.StaticSite"
5761
"url": string

packages/web/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
"@radix-ui/react-tooltip": "^1.1.4",
1919
"@semhub/core": "workspace:*",
2020
"@semhub/workers": "workspace:*",
21+
"@sentry/react": "^8.54.0",
22+
"@sentry/vite-plugin": "^3.1.2",
2123
"@tanstack/react-form": "^0.33.0",
2224
"@tanstack/react-query": "^5.59.6",
2325
"@tanstack/react-router": "^1.63.5",

packages/web/src/components/search/HomepageSearch.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@ export function HomepageSearch() {
2424
<div className="relative">
2525
<HomepageSearchBar />
2626
</div>
27-
2827
{/* Suggested searches section */}
2928
<div className="mx-auto mt-24 grid max-w-xl grid-cols-1 gap-2 sm:mt-8 sm:grid-cols-2 sm:gap-4">
3029
{suggestedSearches.slice(0, 4).map((search) => (

packages/web/src/main.tsx

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,23 @@
11
import "./globals.css";
22

33
import * as Counterscale from "@counterscale/tracker";
4+
import * as Sentry from "@sentry/react";
45
import { QueryClientProvider } from "@tanstack/react-query";
56
import { createRouter, RouterProvider } from "@tanstack/react-router";
67
import { Loader2Icon } from "lucide-react";
78
import { ThemeProvider } from "next-themes";
89
import ReactDOM from "react-dom/client";
910

11+
import { client } from "@/lib/api/client";
1012
import { queryClient } from "@/lib/queryClient";
1113

1214
import { Error } from "./components/Error";
1315
import { NotFound } from "./components/NotFound";
1416
import { routeTree } from "./routeTree.gen";
1517

1618
const sstStage = import.meta.env.VITE_SST_STAGE;
17-
// Initialize Counterscale analytics
19+
20+
// for web analytics
1821
Counterscale.init({
1922
siteId: `semhub-${sstStage}`,
2023
reporterUrl: "https://semhub-prod-counterscale.pages.dev/collect",
@@ -35,6 +38,44 @@ const router = createRouter({
3538
defaultErrorComponent: ({ error }) => <Error error={error} />,
3639
});
3740

41+
Sentry.init({
42+
dsn:
43+
sstStage === "prod"
44+
? "https://566d7949ee5e8265ac7d917d289585bd@o4508764596142080.ingest.us.sentry.io/4508770676637696"
45+
: "https://bf47d2a69dccbb1f44173be530166765@o4508764596142080.ingest.us.sentry.io/4508764610494464",
46+
environment: sstStage,
47+
tunnel: client.sentry.tunnel.$url().toString(),
48+
debug: sstStage === "prod" ? false : true,
49+
integrations: [
50+
Sentry.tanstackRouterBrowserTracingIntegration(router),
51+
Sentry.replayIntegration({
52+
// NOTE: This will disable built-in masking. Only use this if your site has no sensitive data, or if you've already set up other options for masking or blocking relevant data, such as 'ignore', 'block', 'mask' and 'maskFn'.
53+
maskAllText: false,
54+
blockAllMedia: false,
55+
}),
56+
],
57+
// Tracing
58+
tracesSampleRate: sstStage === "prod" ? 0.1 : 1.0, // Capture 10% in prod, 100% elsewhere
59+
// Set 'tracePropagationTargets' to control for which URLs distributed tracing should be enabled
60+
tracePropagationTargets: [
61+
// uat endpoints
62+
"https://api.uat.semhub.dev",
63+
"https://auth.uat.semhub.dev",
64+
// Production endpoints
65+
"https://api.semhub.dev",
66+
"https://auth.semhub.dev",
67+
// stg endpoints
68+
"https://api.stg.semhub.dev",
69+
"https://auth.stg.semhub.dev",
70+
// dev API endpoints with dynamic subdomains
71+
/^https:\/\/api\.[^.]+\.stg\.semhub\.dev$/, // Matches api.{anything}.stg.semhub.dev
72+
/^https:\/\/auth\.[^.]+\.stg\.semhub\.dev$/, // Matches auth.{anything}.stg.semhub.dev
73+
],
74+
// Session Replay - only enabled in production
75+
replaysSessionSampleRate: sstStage === "prod" ? 0.1 : 0, // 10% of sessions in prod, disabled elsewhere
76+
replaysOnErrorSampleRate: sstStage === "prod" ? 1.0 : 0, // 100% of error sessions in prod, disabled elsewhere
77+
});
78+
3879
declare module "@tanstack/react-router" {
3980
interface Register {
4081
router: typeof router;

packages/web/vite.config.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,24 @@
11
import fs from "fs";
22
import path from "path";
3+
import { sentryVitePlugin } from "@sentry/vite-plugin";
34
import { TanStackRouterVite } from "@tanstack/router-plugin/vite";
45
import react from "@vitejs/plugin-react";
56
import { defineConfig } from "vite";
67

7-
// https://vitejs.dev/config/
88
export default defineConfig(() => {
99
return {
10-
plugins: [TanStackRouterVite({}), react()],
10+
plugins: [
11+
TanStackRouterVite({}),
12+
react(),
13+
sentryVitePlugin({
14+
authToken: process.env.SENTRY_AUTH_TOKEN,
15+
org: "coder-aw",
16+
project:
17+
process.env.SST_STAGE === "prod"
18+
? "semhub-web-prod"
19+
: "semhub-web-dev",
20+
}),
21+
],
1122
resolve: {
1223
alias: {
1324
"@/core": path.resolve(__dirname, "../core/src"),
@@ -16,6 +27,7 @@ export default defineConfig(() => {
1627
},
1728
},
1829
build: {
30+
sourcemap: true,
1931
rollupOptions: {
2032
output: {
2133
manualChunks: {

packages/workers/package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,11 @@
66
"build": "tsc --build"
77
},
88
"dependencies": {
9-
"@semhub/core": "workspace:*",
10-
"@semhub/wrangler": "workspace:*",
119
"@hono/zod-validator": "^0.4.1",
1210
"@openauthjs/openauth": "*",
11+
"@semhub/core": "workspace:*",
12+
"@semhub/wrangler": "workspace:*",
13+
"@sentry/cloudflare": "^8.54.0",
1314
"hono": "^4.6.5"
1415
},
1516
"devDependencies": {

packages/workers/src/api.ts

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,28 @@
1+
import type { ExportedHandler } from "@cloudflare/workers-types";
2+
import * as Sentry from "@sentry/cloudflare";
3+
import { Resource } from "sst";
4+
5+
import type { Context } from "./server/app";
16
import { app } from "./server/app";
27

3-
export default {
4-
fetch: app.fetch,
8+
type CloudflareRequest<T = unknown> = Request<T, CfProperties<T>>;
9+
const handler: ExportedHandler<Context> = {
10+
async fetch(request, env, ctx) {
11+
// needed to fix type error
12+
return app.fetch(request as CloudflareRequest, env, ctx);
13+
},
514
};
15+
16+
// Wrap with Sentry
17+
export default Sentry.withSentry(
18+
() => ({
19+
dsn:
20+
Resource.App.stage === "prod"
21+
? "https://8a5572abfbb6f99f6144edf73b98446f@o4508764596142080.ingest.us.sentry.io/4508770682273792"
22+
: "https://d415d30f99a3f43649f2289a054fe5b2@o4508764596142080.ingest.us.sentry.io/4508764598829056",
23+
tracesSampleRate: Resource.App.stage === "prod" ? 0.2 : 1.0,
24+
debug: Resource.App.stage === "prod" ? false : true,
25+
environment: Resource.App.stage,
26+
}),
27+
handler,
28+
);

packages/workers/src/auth/auth.constant.ts

Lines changed: 28 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,18 @@
1+
import { type cors } from "hono/cors";
12
import type { CookieOptions } from "hono/utils/cookie";
23

4+
import {
5+
APP_DOMAIN,
6+
APP_STG_DOMAIN,
7+
APP_UAT_DOMAIN,
8+
LOCAL_DEV_DOMAIN,
9+
STAGES,
10+
} from "@/core/constants/domain.constant";
311
import { GITHUB_SCOPES_PERMISSION } from "@/core/github/permission/oauth";
412

13+
// Extract CORSOptions type from cors function
14+
type CORSOptions = NonNullable<Parameters<typeof cors>[0]>;
15+
516
export const githubLogin = {
617
provider: "github-login" as const,
718
scopes: [
@@ -10,18 +21,13 @@ export const githubLogin = {
1021
],
1122
};
1223

13-
export const APP_DOMAIN = "semhub.dev";
14-
const LOCAL_DEV_DOMAIN = `local.${APP_DOMAIN}`;
15-
const APP_STG_DOMAIN = `stg.${APP_DOMAIN}`;
16-
const APP_UAT_DOMAIN = `uat.${APP_DOMAIN}`;
17-
1824
function getCookieDomain(stage: string) {
1925
switch (stage) {
20-
case "prod":
26+
case STAGES.PROD:
2127
return APP_DOMAIN;
22-
case "uat":
28+
case STAGES.UAT:
2329
return APP_UAT_DOMAIN;
24-
case "stg":
30+
case STAGES.STG:
2531
return APP_STG_DOMAIN;
2632
default:
2733
// For local development, we set the cookie on the parent domain (.semhub.dev) because:
@@ -34,7 +40,7 @@ function getCookieDomain(stage: string) {
3440
}
3541

3642
function isLocalDev(stage: string): boolean {
37-
return stage !== "prod" && stage !== "stg" && stage !== "uat";
43+
return stage !== STAGES.PROD && stage !== STAGES.STG && stage !== STAGES.UAT;
3844
}
3945

4046
export function getCookieOptions(stage: string): CookieOptions {
@@ -55,11 +61,15 @@ export function getAuthServerCORS() {
5561
credentials: false,
5662
// can use wildcard if credentials: false is used
5763
origin: `https://*.${APP_DOMAIN}`,
58-
allowHeaders: ["Content-Type"],
64+
allowHeaders: [
65+
"Content-Type",
66+
"sentry-trace", // Allow Sentry tracing headers
67+
"baggage", // Allow Sentry baggage header
68+
],
5969
allowMethods: ["POST", "GET", "OPTIONS"],
6070
exposeHeaders: ["Content-Length", "Access-Control-Allow-Origin"],
6171
maxAge: 600,
62-
};
72+
} satisfies CORSOptions;
6373
}
6474

6575
export function getApiServerCORS(stage: string) {
@@ -76,9 +86,14 @@ export function getApiServerCORS(stage: string) {
7686
return {
7787
credentials: true,
7888
origin: origins,
79-
allowHeaders: ["Content-Type", "Authorization"],
89+
allowHeaders: [
90+
"Content-Type",
91+
"Authorization",
92+
"sentry-trace", // Allow Sentry tracing headers
93+
"baggage", // Allow Sentry baggage header
94+
],
8095
allowMethods: ["POST", "GET", "OPTIONS"],
8196
exposeHeaders: ["Content-Length", "Access-Control-Allow-Origin"],
8297
maxAge: 600,
83-
};
98+
} satisfies CORSOptions;
8499
}

packages/workers/src/auth/authenticator.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,13 @@ import { Hono } from "hono";
55
import { cors } from "hono/cors";
66
import { Resource } from "sst";
77

8+
import { APP_DOMAIN } from "@/core/constants/domain.constant";
89
import { tokensetRawSchema } from "@/core/github/schema.oauth";
910
import { User } from "@/core/user";
1011
import { parseHostname } from "@/core/util/url";
1112

1213
import { getDeps } from "../deps";
13-
import { APP_DOMAIN, getAuthServerCORS, githubLogin } from "./auth.constant";
14+
import { getAuthServerCORS, githubLogin } from "./auth.constant";
1415
import { subjects } from "./subjects";
1516

1617
const app = new Hono();

0 commit comments

Comments
 (0)