mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-05-11 08:42:39 +02:00
Merge pull request #874 from MODSetter/dev
feat: enhance error handling with PostHog integration
This commit is contained in:
commit
1c7c21386f
5 changed files with 122 additions and 8 deletions
32
surfsense_web/app/error.tsx
Normal file
32
surfsense_web/app/error.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
28
surfsense_web/app/global-error.tsx
Normal file
28
surfsense_web/app/global-error.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
37
surfsense_web/instrumentation.ts
Normal file
37
surfsense_web/instrumentation.ts
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue