Merge pull request #874 from MODSetter/dev

feat: enhance error handling with PostHog integration
This commit is contained in:
Rohan Verma 2026-03-12 01:31:47 -07:00 committed by GitHub
commit 1c7c21386f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 122 additions and 8 deletions

View file

@ -0,0 +1,32 @@
"use client";
import posthog from "posthog-js";
import { useEffect } from "react";
export default function Error({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
useEffect(() => {
posthog.captureException(error);
}, [error]);
return (
<div className="flex min-h-[50vh] flex-col items-center justify-center gap-4 text-center">
<h2 className="text-2xl font-semibold">Something went wrong</h2>
<p className="text-muted-foreground max-w-md">
An unexpected error occurred. Please try again.
</p>
<button
type="button"
onClick={reset}
className="rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90 transition-colors"
>
Try again
</button>
</div>
);
}

View file

@ -0,0 +1,28 @@
"use client";
import posthog from "posthog-js";
import NextError from "next/error";
import { useEffect } from "react";
export default function GlobalError({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
useEffect(() => {
posthog.captureException(error);
}, [error]);
return (
<html lang="en">
<body>
<NextError statusCode={0} />
<button type="button" onClick={reset}>
Try again
</button>
</body>
</html>
);
}

View file

@ -0,0 +1,37 @@
import type { Instrumentation } from "next";
const POSTHOG_COOKIE_RE = /ph_phc_.*?_posthog=([^;]+)/;
export function register() {
// No-op — PostHog server client is lazily initialized
}
export const onRequestError: Instrumentation.onRequestError = async (err, request) => {
if (process.env.NEXT_RUNTIME === "nodejs") {
const { default: PostHogClient } = await import("./lib/posthog/server");
try {
const posthog = PostHogClient();
let distinctId: string | undefined;
const rawCookie = request.headers.cookie;
if (rawCookie) {
const cookieString = Array.isArray(rawCookie) ? rawCookie.join("; ") : rawCookie;
const postHogCookieMatch = cookieString.match(POSTHOG_COOKIE_RE);
if (postHogCookieMatch?.[1]) {
try {
const decodedCookie = decodeURIComponent(postHogCookieMatch[1]);
const postHogData = JSON.parse(decodedCookie);
distinctId = postHogData.distinct_id;
} catch {
// Cookie parsing failed — capture without distinct_id
}
}
}
await posthog.captureException(err, distinctId);
} catch {
// PostHog server capture failed — don't let it affect the request
}
}
};

View file

@ -1,3 +1,4 @@
import posthog from "posthog-js";
import type { ZodType } from "zod"; import type { ZodType } from "zod";
import { getBearerToken, handleUnauthorized, refreshAccessToken } from "../auth-utils"; import { getBearerToken, handleUnauthorized, refreshAccessToken } from "../auth-utils";
import { AppError, AuthenticationError, AuthorizationError, NotFoundError } from "../error"; import { AppError, AuthenticationError, AuthorizationError, NotFoundError } from "../error";
@ -231,6 +232,20 @@ class BaseApiService {
return data; return data;
} catch (error) { } catch (error) {
console.error("Request failed:", JSON.stringify(error)); console.error("Request failed:", JSON.stringify(error));
if (!(error instanceof AuthenticationError)) {
try {
posthog.captureException(error, {
api_url: url,
api_method: options?.method ?? "GET",
...(error instanceof AppError && {
status_code: error.status,
status_text: error.statusText,
}),
});
} catch {
// PostHog capture failed — don't block the error flow
}
}
throw error; throw error;
} }
} }

View file

@ -1,17 +1,19 @@
import { PostHog } from "posthog-node"; import { PostHog } from "posthog-node";
let posthogInstance: PostHog | null = null;
export default function PostHogClient() { export default function PostHogClient() {
if (!process.env.NEXT_PUBLIC_POSTHOG_KEY) { if (!process.env.NEXT_PUBLIC_POSTHOG_KEY) {
throw new Error("NEXT_PUBLIC_POSTHOG_KEY is not set"); throw new Error("NEXT_PUBLIC_POSTHOG_KEY is not set");
} }
const posthogClient = new PostHog(process.env.NEXT_PUBLIC_POSTHOG_KEY, { if (!posthogInstance) {
posthogInstance = new PostHog(process.env.NEXT_PUBLIC_POSTHOG_KEY, {
host: process.env.NEXT_PUBLIC_POSTHOG_HOST, host: process.env.NEXT_PUBLIC_POSTHOG_HOST,
// Because server-side functions in Next.js can be short-lived,
// we set flushAt to 1 and flushInterval to 0 to ensure events are sent immediately
flushAt: 1, flushAt: 1,
flushInterval: 0, flushInterval: 0,
}); });
}
return posthogClient;
return posthogInstance;
} }