Skip to content

Commit fc02109

Browse files
authored
feat(observability): configure PostHog error tracking (#385)
1 parent 3bc4d6a commit fc02109

12 files changed

Lines changed: 261 additions & 17 deletions

File tree

.env.example

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,14 @@ NEXT_PUBLIC_E2B_DOMAIN=e2b.dev
9393
### PostHog analytics key
9494
# NEXT_PUBLIC_POSTHOG_KEY=
9595

96+
### PostHog source map upload (build-time only, used by @posthog/nextjs-config).
97+
### Configure these as build env vars in the Vercel project settings (production,
98+
### and optionally preview) — they are not needed for local development.
99+
### POSTHOG_API_KEY is a personal API key with "error tracking: write" and
100+
### "organization: read" scopes. POSTHOG_PROJECT_ID is found in project settings.
101+
# POSTHOG_API_KEY=
102+
# POSTHOG_PROJECT_ID=
103+
96104
### Enable billing features: set to 1 to enable
97105
### When enabled, both BILLING_API_URL and NEXT_PUBLIC_STRIPE_BILLING_URL must be provided
98106
# NEXT_PUBLIC_INCLUDE_BILLING=0

bun.lock

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

next.config.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
// NOTE: related to src/configs/rewrites.ts
22
import path from 'node:path'
3+
import { withPostHogConfig } from '@posthog/nextjs-config'
34
import type { NextConfig } from 'next'
45
import { DOCUMENTATION_DOMAIN } from './src/configs/documentation'
56

@@ -140,4 +141,14 @@ const config: NextConfig = {
140141
skipTrailingSlashRedirect: true,
141142
}
142143

143-
export default config
144+
export default withPostHogConfig(config, {
145+
personalApiKey: process.env.POSTHOG_API_KEY ?? '',
146+
projectId: process.env.POSTHOG_PROJECT_ID,
147+
host: 'https://us.posthog.com',
148+
sourcemaps: {
149+
enabled: Boolean(process.env.POSTHOG_API_KEY),
150+
releaseName: 'dashboard',
151+
releaseVersion: process.env.VERCEL_GIT_COMMIT_SHA,
152+
deleteAfterUpload: true,
153+
},
154+
})

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,7 @@
145145
"devDependencies": {
146146
"@biomejs/biome": "^2.4.4",
147147
"@playwright/test": "^1.59.1",
148+
"@posthog/nextjs-config": "^1.9.55",
148149
"@tailwindcss/postcss": "^4.0.15",
149150
"@types/bun": "^1.2.5",
150151
"@types/node": "22.10.10",

src/app/dashboard/[teamSlug]/layout.tsx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { DASHBOARD_TEAMS_LIST_QUERY_OPTIONS } from '@/core/application/teams/que
1010
import { DASHBOARD_USER_PROFILE_QUERY_OPTIONS } from '@/core/application/user/queries'
1111
import { auth } from '@/core/server/auth'
1212
import DashboardLayoutView from '@/features/dashboard/layouts/layout'
13+
import { DashboardPostHogErrorBoundary } from '@/features/dashboard/posthog-error-boundary'
1314
import Sidebar from '@/features/dashboard/sidebar/sidebar'
1415
import { OryPostHogIdentityBridge } from '@/features/ory-posthog-identity-bridge'
1516
import { HydrateClient, prefetchAsync, trpc } from '@/trpc/server'
@@ -73,9 +74,11 @@ export default async function DashboardLayout({
7374
<div className="flex h-full max-h-full min-h-0 w-full flex-1 overflow-hidden">
7475
<Sidebar />
7576
<SidebarInset>
76-
<DashboardLayoutView params={params}>
77-
{children}
78-
</DashboardLayoutView>
77+
<DashboardPostHogErrorBoundary>
78+
<DashboardLayoutView params={params}>
79+
{children}
80+
</DashboardLayoutView>
81+
</DashboardPostHogErrorBoundary>
7982
</SidebarInset>
8083
</div>
8184
</div>

src/app/global-error.tsx

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,40 @@
11
'use client'
22

3+
import posthog from 'posthog-js'
4+
import { useEffect } from 'react'
35
import ErrorBoundary from '@/ui/error'
46

57
export default function GlobalError({
68
error,
79
}: {
810
error: Error & { digest?: string }
911
}) {
12+
useEffect(() => {
13+
const key = process.env.NEXT_PUBLIC_POSTHOG_KEY
14+
if (!key) return
15+
16+
// global-error renders outside RootLayout/ClientProviders, so PostHogProvider
17+
// never ran posthog.init. Initialize a minimal client here so root-layout and
18+
// provider failures are still reported instead of silently dropped.
19+
if (!posthog.__loaded) {
20+
posthog.init(key, {
21+
api_host: '/ph-proxy',
22+
ui_host: 'https://us.posthog.com',
23+
capture_exceptions: false,
24+
})
25+
posthog.register({
26+
environment: process.env.NEXT_PUBLIC_VERCEL_ENV ?? 'development',
27+
})
28+
}
29+
30+
posthog.captureException(error)
31+
}, [error])
32+
1033
return (
1134
<ErrorBoundary
1235
description="Sorry, something went wrong with the application."
1336
error={error}
37+
report={false}
1438
/>
1539
)
1640
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
'use client'
2+
3+
import {
4+
PostHogErrorBoundary,
5+
type PostHogErrorBoundaryFallbackProps,
6+
} from 'posthog-js/react'
7+
import { ErrorIndicator } from '@/ui/error-indicator'
8+
import Frame from '@/ui/frame'
9+
10+
// Not the shared ErrorBoundary: it calls captureException, which would
11+
// double-report since PostHogErrorBoundary already reports the caught error.
12+
function Fallback({ error }: PostHogErrorBoundaryFallbackProps) {
13+
const message = error instanceof Error ? error.message : String(error)
14+
15+
return (
16+
<div className="flex h-full w-full items-center justify-center">
17+
<Frame>
18+
<ErrorIndicator
19+
description="Sorry, something went wrong."
20+
message={message}
21+
className="border-none"
22+
/>
23+
</Frame>
24+
</div>
25+
)
26+
}
27+
28+
export function DashboardPostHogErrorBoundary({
29+
children,
30+
}: {
31+
children: React.ReactNode
32+
}) {
33+
return (
34+
<PostHogErrorBoundary fallback={Fallback}>{children}</PostHogErrorBoundary>
35+
)
36+
}

src/features/dashboard/shared/route-error.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ export function DashboardRouteError({
3434
return (
3535
<ErrorBoundary
3636
description="Sorry, something went wrong with this page."
37+
className="min-h-svh"
3738
error={error}
3839
onRetry={() => {
3940
router.refresh()

src/features/posthog-provider.tsx

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { usePathname } from 'next/navigation'
12
import posthog, { type Survey } from 'posthog-js'
23
import { PostHogProvider as PHProvider } from 'posthog-js/react'
34
import { createContext, useContext, useEffect, useState } from 'react'
@@ -31,12 +32,20 @@ export function PostHogProvider({
3132
children: React.ReactNode
3233
enabled: boolean
3334
}) {
35+
const pathname = usePathname()
3436
const [dashboardFeedbackSurvey, setDashboardFeedbackSurvey] =
3537
useState<Survey | null>(null)
3638
const [isInitialized, setIsInitialized] = useState(false)
3739

40+
// Only track the dashboard app — not auth, marketing, or proxied (docs/blog)
41+
// paths. PostHog initializes lazily once the user reaches a /dashboard route.
42+
const shouldInit = enabled && !!pathname?.startsWith('/dashboard')
43+
3844
useEffect(() => {
39-
if (!enabled || !process.env.NEXT_PUBLIC_POSTHOG_KEY) {
45+
if (!shouldInit || !process.env.NEXT_PUBLIC_POSTHOG_KEY) {
46+
return
47+
}
48+
if (posthog.__loaded) {
4049
return
4150
}
4251

@@ -48,6 +57,11 @@ export function PostHogProvider({
4857
// https://posthog.com/docs/libraries/next-js#configuring-a-reverse-proxy-to-posthog
4958
api_host: '/ph-proxy',
5059
ui_host: 'https://us.posthog.com',
60+
capture_exceptions: {
61+
capture_unhandled_errors: true,
62+
capture_unhandled_rejections: true,
63+
capture_console_errors: false,
64+
},
5165
advanced_enable_surveys: true,
5266
disable_session_recording: process.env.NODE_ENV !== 'production',
5367
advanced_disable_toolbar_metrics: true,
@@ -57,6 +71,10 @@ export function PostHogProvider({
5771
},
5872
})
5973

74+
posthog.register({
75+
environment: process.env.NEXT_PUBLIC_VERCEL_ENV ?? 'development',
76+
})
77+
6078
posthog.getSurveys((surveys) => {
6179
for (const survey of surveys) {
6280
switch (survey.id) {
@@ -67,7 +85,7 @@ export function PostHogProvider({
6785
}
6886
setIsInitialized(true)
6987
})
70-
}, [enabled])
88+
}, [shouldInit])
7189

7290
return (
7391
<AppPostHogContext.Provider

src/lib/env.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ export const serverSchema = z.object({
1414

1515
TURNSTILE_SECRET_KEY: z.string().optional(),
1616

17+
POSTHOG_API_KEY: z.string().min(1).optional(),
18+
POSTHOG_PROJECT_ID: z.string().min(1).optional(),
19+
1720
AUTH_PROVIDER: z.enum(['supabase', 'ory']),
1821
AUTH_SECRET: z.string().min(1).optional(),
1922
AUTH_TRUST_HOST: z.string().optional(),
@@ -65,6 +68,9 @@ export const clientSchema = z.object({
6568
.string()
6669
.min(1)
6770
.optional(),
71+
NEXT_PUBLIC_VERCEL_ENV: z
72+
.enum(['production', 'preview', 'development'])
73+
.optional(),
6874

6975
NEXT_PUBLIC_INCLUDE_BILLING: z.string().optional(),
7076
NEXT_PUBLIC_INCLUDE_ARGUS: z.string().optional(),

0 commit comments

Comments
 (0)