mirror of
https://github.com/supermemoryai/supermemory.git
synced 2026-10-22 05:26:17 +00:00
ErrorTrackingProvider wraps the whole app and keyed its window "error" and "unhandledrejection" listener effect on trackError. trackError is rebuilt on every render because it closes over posthog, session and pathname, so the two global listeners were torn down and re-added on every render of the provider, including on each navigation and session tick. Hold the latest trackError in a ref and register the listeners once for the provider's lifetime, dispatching through the ref so reports still use current posthog/session/pathname context.
203 lines
4.6 KiB
TypeScript
203 lines
4.6 KiB
TypeScript
"use client"
|
|
|
|
import { usePathname } from "next/navigation"
|
|
import { useEffect, useRef } from "react"
|
|
import { useSession } from "./auth"
|
|
import { usePostHog } from "./posthog"
|
|
|
|
export function useErrorTracking() {
|
|
const posthog = usePostHog()
|
|
const { data: session } = useSession()
|
|
const pathname = usePathname()
|
|
|
|
const trackError = (
|
|
error: Error | unknown,
|
|
context?: Record<string, any>,
|
|
) => {
|
|
if (!posthog.__loaded) return
|
|
const errorDetails = {
|
|
error_message: error instanceof Error ? error.message : String(error),
|
|
error_stack: error instanceof Error ? error.stack : undefined,
|
|
error_name: error instanceof Error ? error.name : "Unknown",
|
|
pathname,
|
|
user_id: session?.user?.id,
|
|
user_email: session?.user?.email,
|
|
timestamp: new Date().toISOString(),
|
|
...context,
|
|
}
|
|
|
|
posthog.capture("error_occurred", errorDetails)
|
|
}
|
|
|
|
const trackApiError = (
|
|
error: Error | unknown,
|
|
endpoint: string,
|
|
method: string,
|
|
) => {
|
|
trackError(error, {
|
|
error_type: "api_error",
|
|
api_endpoint: endpoint,
|
|
api_method: method,
|
|
})
|
|
}
|
|
|
|
const trackComponentError = (
|
|
error: Error | unknown,
|
|
componentName: string,
|
|
) => {
|
|
trackError(error, {
|
|
error_type: "component_error",
|
|
component_name: componentName,
|
|
})
|
|
}
|
|
|
|
const trackValidationError = (
|
|
error: Error | unknown,
|
|
formName: string,
|
|
field?: string,
|
|
) => {
|
|
trackError(error, {
|
|
error_type: "validation_error",
|
|
form_name: formName,
|
|
field_name: field,
|
|
})
|
|
}
|
|
|
|
return {
|
|
trackError,
|
|
trackApiError,
|
|
trackComponentError,
|
|
trackValidationError,
|
|
}
|
|
}
|
|
|
|
// Global error boundary component
|
|
export function ErrorTrackingProvider({
|
|
children,
|
|
}: {
|
|
children: React.ReactNode
|
|
}) {
|
|
const { trackError } = useErrorTracking()
|
|
|
|
// trackError is a fresh closure every render (it captures posthog, session and
|
|
// pathname), so keying the listener effect on it re-subscribed the two global
|
|
// window listeners on every render of this app-wide provider. Hold the latest
|
|
// trackError in a ref and register the listeners once for the provider's life.
|
|
const trackErrorRef = useRef(trackError)
|
|
useEffect(() => {
|
|
trackErrorRef.current = trackError
|
|
}, [trackError])
|
|
|
|
useEffect(() => {
|
|
// Global error handler for unhandled errors
|
|
const handleError = (event: ErrorEvent) => {
|
|
trackErrorRef.current(event.error, {
|
|
error_type: "global_error",
|
|
source: "window_error",
|
|
filename: event.filename,
|
|
lineno: event.lineno,
|
|
colno: event.colno,
|
|
})
|
|
}
|
|
|
|
// Global handler for unhandled promise rejections
|
|
const handleUnhandledRejection = (event: PromiseRejectionEvent) => {
|
|
trackErrorRef.current(event.reason, {
|
|
error_type: "unhandled_promise_rejection",
|
|
source: "promise_rejection",
|
|
})
|
|
}
|
|
|
|
window.addEventListener("error", handleError)
|
|
window.addEventListener("unhandledrejection", handleUnhandledRejection)
|
|
|
|
return () => {
|
|
window.removeEventListener("error", handleError)
|
|
window.removeEventListener("unhandledrejection", handleUnhandledRejection)
|
|
}
|
|
}, [])
|
|
|
|
return <>{children}</>
|
|
}
|
|
|
|
// Hook for tracking user interactions
|
|
export function useInteractionTracking() {
|
|
const posthog = usePostHog()
|
|
const { data: session } = useSession()
|
|
const pathname = usePathname()
|
|
|
|
const trackInteraction = (action: string, details?: Record<string, any>) => {
|
|
if (!posthog.__loaded) return
|
|
posthog.capture("user_interaction", {
|
|
action,
|
|
pathname,
|
|
user_id: session?.user?.id,
|
|
timestamp: new Date().toISOString(),
|
|
...details,
|
|
})
|
|
}
|
|
|
|
const trackFormSubmission = (
|
|
formName: string,
|
|
success: boolean,
|
|
details?: Record<string, any>,
|
|
) => {
|
|
if (!posthog.__loaded) return
|
|
posthog.capture("form_submission", {
|
|
form_name: formName,
|
|
success,
|
|
pathname,
|
|
user_id: session?.user?.id,
|
|
timestamp: new Date().toISOString(),
|
|
...details,
|
|
})
|
|
}
|
|
|
|
const trackButtonClick = (buttonName: string, context?: string) => {
|
|
trackInteraction("button_click", {
|
|
button_name: buttonName,
|
|
context,
|
|
})
|
|
}
|
|
|
|
const trackLinkClick = (
|
|
url: string,
|
|
linkText?: string,
|
|
external?: boolean,
|
|
) => {
|
|
trackInteraction("link_click", {
|
|
url,
|
|
link_text: linkText,
|
|
external,
|
|
})
|
|
}
|
|
|
|
const trackModalOpen = (modalName: string) => {
|
|
trackInteraction("modal_open", {
|
|
modal_name: modalName,
|
|
})
|
|
}
|
|
|
|
const trackModalClose = (modalName: string) => {
|
|
trackInteraction("modal_close", {
|
|
modal_name: modalName,
|
|
})
|
|
}
|
|
|
|
const trackTabChange = (fromTab: string, toTab: string) => {
|
|
trackInteraction("tab_change", {
|
|
from_tab: fromTab,
|
|
to_tab: toTab,
|
|
})
|
|
}
|
|
|
|
return {
|
|
trackInteraction,
|
|
trackFormSubmission,
|
|
trackButtonClick,
|
|
trackLinkClick,
|
|
trackModalOpen,
|
|
trackModalClose,
|
|
trackTabChange,
|
|
}
|
|
}
|