Skip to content

Commit 8e15a0a

Browse files
committed
feat: integrate Sentry for enhanced error tracking and logging
- Added Sentry for both client and server error tracking, capturing exceptions and logging relevant information. - Updated logging middleware to include request and response timing, with Sentry breadcrumbs for better observability. - Enhanced database logging with Sentry integration to capture SQL queries and errors. - Updated vars.example.yml to include Sentry DSN and related configuration for improved observability.
1 parent 49d60f4 commit 8e15a0a

File tree

13 files changed

+1022
-39
lines changed

13 files changed

+1022
-39
lines changed

.cursorignore

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,2 @@
1-
.env
21
public/**
32
drizzle/meta/

infra/ansible/group_vars/constructa/vars.example.yml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,3 +97,10 @@ constructa_env:
9797
POLAR_PRODUCT_BUSINESS_MONTHLY: ''
9898
POLAR_PRODUCT_CREDITS_50: ''
9999
POLAR_PRODUCT_CREDITS_100: ''
100+
101+
SENTRY_DSN: ''
102+
VITE_SENTRY_DSN: ''
103+
VITE_SENTRY_TRACES_SAMPLE_RATE: '1.0'
104+
VITE_SENTRY_REPLAYS_SESSION_SAMPLE_RATE: '0.1'
105+
VITE_SENTRY_REPLAYS_ON_ERROR_SAMPLE_RATE: '1.0'
106+
SENTRY_TRACES_SAMPLE_RATE: '1.0'

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,8 @@
7070
"@radix-ui/react-toggle": "^1.1.10",
7171
"@radix-ui/react-toggle-group": "^1.1.11",
7272
"@radix-ui/react-tooltip": "^1.2.8",
73+
"@sentry/node": "^8.55.0",
74+
"@sentry/react": "^8.55.0",
7375
"@t3-oss/env-core": "^0.13.8",
7476
"@tanstack/react-query": "^5.89.0",
7577
"@tanstack/react-query-devtools": "^5.89.0",

pnpm-lock.yaml

Lines changed: 687 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/components/DefaultCatchBoundary.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { ErrorComponent, Link, rootRouteId, useMatch, useRouter } from "@tanstack/react-router"
22
import type { ErrorComponentProps } from "@tanstack/react-router"
3+
import * as Sentry from "@sentry/react"
34

45
export function DefaultCatchBoundary({ error }: ErrorComponentProps) {
56
const router = useRouter()
@@ -9,6 +10,11 @@ export function DefaultCatchBoundary({ error }: ErrorComponentProps) {
910
})
1011

1112
console.error("DefaultCatchBoundary Error:", error)
13+
try {
14+
Sentry.captureException(error)
15+
} catch {
16+
// ignore if Sentry not configured
17+
}
1218

1319
return (
1420
<div className="flex min-w-0 flex-1 flex-col items-center justify-center gap-6 p-4">

src/components/Header.tsx

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,33 @@
11
import { SignedIn, SignedOut } from '@daveyplate/better-auth-ui';
22
import { Link } from '@tanstack/react-router';
3+
import * as Sentry from '@sentry/react';
4+
import { useCallback } from 'react';
5+
import { ClientOnly } from './client-only';
36
import { ModeToggle } from './mode-toggle';
47
import { Button } from './ui/button';
5-
import { ClientOnly } from './client-only';
8+
9+
function DevSentryButton() {
10+
const handleClick = useCallback(() => {
11+
const err = new Error('Manual Sentry test (client)')
12+
console.info('[sentry] dispatching manual client error for verification')
13+
Sentry.captureException(err)
14+
}, [])
15+
16+
if (!import.meta.env.DEV) return null
17+
18+
return (
19+
<ClientOnly fallback={null}>
20+
<Button
21+
variant="outline"
22+
size="sm"
23+
onClick={handleClick}
24+
className="text-xs"
25+
>
26+
Test Sentry
27+
</Button>
28+
</ClientOnly>
29+
)
30+
}
631

732
export function Header() {
833
return (
@@ -62,6 +87,8 @@ export function Header() {
6287
<ModeToggle />
6388
</ClientOnly>
6489

90+
<DevSentryButton />
91+
6592
<SignedOut>
6693
<Link to="/auth/$pathname" params={{ pathname: 'sign-in' }}>
6794
<Button className="rounded-full bg-primary px-6 font-medium text-primary-foreground text-sm hover:bg-primary/90">

src/db/db-config.ts

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
import { drizzle } from 'drizzle-orm/postgres-js';
22
import postgres from 'postgres';
33

4-
import * as schema from './schema';
4+
import * as schema from './schema'
55

6-
const connectionString = process.env.DATABASE_URL;
6+
import { DefaultLogger, LogWriter } from 'drizzle-orm/logger'
7+
import * as Sentry from '@sentry/node'
8+
import { redact } from '~/lib/security/token-redaction'
9+
10+
const connectionString = process.env.DATABASE_URL
711

812
if (!connectionString) {
913
throw new Error('DATABASE_URL is not defined');
@@ -14,9 +18,32 @@ const queryClient = postgres(connectionString, {
1418
// rejects prepared statements (e.g. Cloudflare tunnels). Flip to true if your
1519
// environment prefers prepared statements.
1620
prepare: false,
17-
});
21+
})
22+
23+
class SentryQueryWriter implements LogWriter {
24+
write(message: string) {
25+
const msg = redact(message)
26+
try {
27+
Sentry.addBreadcrumb({
28+
category: 'db.sql',
29+
level: 'info',
30+
message: msg,
31+
})
32+
} catch {
33+
// no-op if Sentry not initialized
34+
}
35+
// Also echo in dev to stdout to aid local debugging
36+
if (process.env.NODE_ENV !== 'production') {
37+
// eslint-disable-next-line no-console
38+
console.debug('[SQL]', msg)
39+
}
40+
}
41+
}
42+
43+
const drizzleLogger = new DefaultLogger({ writer: new SentryQueryWriter() })
1844

1945
export const db = drizzle(queryClient, {
2046
schema,
2147
casing: 'snake_case',
22-
});
48+
logger: drizzleLogger,
49+
})
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import * as Sentry from '@sentry/react'
2+
import { redact } from '~/lib/security/token-redaction'
3+
4+
const dsn = import.meta.env.VITE_SENTRY_DSN as string | undefined
5+
6+
if (dsn) {
7+
Sentry.init({
8+
dsn,
9+
environment: import.meta.env.MODE,
10+
debug: import.meta.env.DEV,
11+
// v8-style integrations
12+
integrations: [
13+
Sentry.browserTracingIntegration(),
14+
Sentry.captureConsoleIntegration({ levels: ['error', 'warn'] }),
15+
// Added but sampling below keeps Replay off unless errors occur
16+
Sentry.replayIntegration(),
17+
],
18+
// Keep light by default; override via VITE_* env if needed
19+
tracesSampleRate: Number(import.meta.env.VITE_SENTRY_TRACES_SAMPLE_RATE ?? '0.1'),
20+
replaysSessionSampleRate: Number(
21+
import.meta.env.VITE_SENTRY_REPLAYS_SESSION_SAMPLE_RATE ?? '0.0',
22+
),
23+
replaysOnErrorSampleRate: Number(
24+
import.meta.env.VITE_SENTRY_REPLAYS_ON_ERROR_SAMPLE_RATE ?? '1.0',
25+
),
26+
tracePropagationTargets: ((): (string | RegExp)[] => {
27+
const targets: (string | RegExp)[] = [
28+
/^https?:\/\/localhost(?::\d+)?/,
29+
/^https?:\/\/(.*\.)?ex0\.dev/i,
30+
]
31+
32+
const baseUrl = import.meta.env.VITE_BASE_URL
33+
if (baseUrl) targets.push(baseUrl)
34+
if (typeof window !== 'undefined') targets.push(window.location.origin)
35+
36+
return targets
37+
})(),
38+
beforeBreadcrumb(breadcrumb) {
39+
try {
40+
if (breadcrumb?.message) breadcrumb.message = redact(String(breadcrumb.message))
41+
if (breadcrumb?.data) {
42+
breadcrumb.data = JSON.parse(
43+
JSON.stringify(breadcrumb.data, (k, v) =>
44+
typeof v === 'string' ? redact(v) : v,
45+
),
46+
)
47+
}
48+
} catch {
49+
// no-op
50+
}
51+
return breadcrumb
52+
},
53+
beforeSend(event) {
54+
try {
55+
if (event.request?.url) event.request.url = redact(event.request.url)
56+
if (event.request?.headers) {
57+
for (const [k, v] of Object.entries(event.request.headers)) {
58+
if (typeof v === 'string') (event.request.headers as any)[k] = redact(v)
59+
}
60+
}
61+
// never send cookies from the browser
62+
if (event.request) (event.request as any).cookies = undefined
63+
64+
if (event.exception?.values) {
65+
for (const ex of event.exception.values) {
66+
if (ex.value) ex.value = redact(ex.value)
67+
}
68+
}
69+
} catch {
70+
// no-op
71+
}
72+
return event
73+
},
74+
})
75+
} else if (import.meta.env.DEV) {
76+
console.warn('[sentry] VITE_SENTRY_DSN missing; client telemetry disabled')
77+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import * as Sentry from '@sentry/node'
2+
import { redact } from '~/lib/security/token-redaction'
3+
4+
const dsn = process.env.SENTRY_DSN
5+
6+
if (dsn) {
7+
Sentry.init({
8+
dsn,
9+
environment: process.env.NODE_ENV,
10+
tracesSampleRate: Number(process.env.SENTRY_TRACES_SAMPLE_RATE ?? '0.1'),
11+
integrations: [
12+
Sentry.httpIntegration(),
13+
Sentry.captureConsoleIntegration({ levels: ['error', 'warn'] }),
14+
],
15+
beforeBreadcrumb(breadcrumb) {
16+
try {
17+
if (breadcrumb?.message) breadcrumb.message = redact(String(breadcrumb.message))
18+
if (breadcrumb?.data) {
19+
breadcrumb.data = JSON.parse(
20+
JSON.stringify(breadcrumb.data, (k, v) =>
21+
typeof v === 'string' ? redact(v) : v,
22+
),
23+
)
24+
}
25+
} catch {
26+
// no-op
27+
}
28+
return breadcrumb
29+
},
30+
beforeSend(event) {
31+
try {
32+
if (event.request?.url) event.request.url = redact(event.request.url)
33+
if (event.request?.headers) {
34+
for (const [k, v] of Object.entries(event.request.headers)) {
35+
if (typeof v === 'string') (event.request.headers as any)[k] = redact(v)
36+
}
37+
}
38+
if (event.exception?.values) {
39+
for (const ex of event.exception.values) {
40+
if (ex.value) ex.value = redact(ex.value)
41+
}
42+
}
43+
} catch {
44+
// no-op
45+
}
46+
return event
47+
},
48+
})
49+
}
50+
51+
export { Sentry }

src/router.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@ import { NotFound } from './components/NotFound';
66
import { routeTree } from './routeTree.gen';
77

88
// Initialize browser-echo for TanStack Start (manual import required)
9-
if (import.meta.env.DEV && typeof window !== 'undefined') {
10-
void import('virtual:browser-echo');
11-
}
9+
// if (import.meta.env.DEV && typeof window !== 'undefined') {
10+
// void import('virtual:browser-echo');
11+
// }
1212

1313
export function getRouter() {
1414
const queryClient = new QueryClient();

0 commit comments

Comments
 (0)