diff --git a/apps/web/src/components/features/global/GlobalErrorCard.tsx b/apps/web/src/components/features/global/GlobalErrorCard.tsx index f38b233ec..204ef1f33 100644 --- a/apps/web/src/components/features/global/GlobalErrorCard.tsx +++ b/apps/web/src/components/features/global/GlobalErrorCard.tsx @@ -49,7 +49,12 @@ export const GlobalErrorCard: ErrorRouteComponent = ({ error }) => { const isPosthogLoaded = posthog.__loaded; if (isPosthogLoaded) { - posthog.captureException(error); + try { + posthog.captureException(error); + } catch (captureError) { + // Silently handle PostHog capture errors (likely due to ad blocker) + console.warn('PostHog capture failed:', captureError); + } } }, [error]); diff --git a/apps/web/src/context/Posthog/BusterPosthogProvider.tsx b/apps/web/src/context/Posthog/BusterPosthogProvider.tsx index 480e798b0..b22167866 100644 --- a/apps/web/src/context/Posthog/BusterPosthogProvider.tsx +++ b/apps/web/src/context/Posthog/BusterPosthogProvider.tsx @@ -16,6 +16,147 @@ const version = packageJson.version; const POSTHOG_KEY = env.VITE_PUBLIC_POSTHOG_KEY; const DEBUG_POSTHOG = false; +// PostHog failure detection constants +const MAX_CONSECUTIVE_FAILURES = 3; +const FAILURE_TIMEOUT = 5000; // 5 seconds + +// Global state for failure tracking +let consecutiveFailures = 0; +let isPosthogBlocked = false; +let failureTimeouts: NodeJS.Timeout[] = []; + +/** + * Monitors PostHog network requests for failures and shuts down PostHog + * if too many consecutive failures are detected (likely due to ad blockers) + */ +const setupPosthogFailureDetection = (posthog: typeof import('posthog-js').default) => { + if (isPosthogBlocked) { + return; + } + + // Store original methods + const originalCapture = posthog.capture; + const originalIdentify = posthog.identify; + const originalGroup = posthog.group; + + const handleNetworkFailure = () => { + consecutiveFailures++; + console.warn( + `PostHog network failure detected (${consecutiveFailures}/${MAX_CONSECUTIVE_FAILURES})` + ); + + if (consecutiveFailures >= MAX_CONSECUTIVE_FAILURES) { + console.warn('PostHog blocked - shutting down to prevent network bloat'); + shutdownPosthog(posthog); + } + }; + + const handleNetworkSuccess = () => { + if (consecutiveFailures > 0) { + console.info('PostHog network request succeeded - resetting failure count'); + consecutiveFailures = 0; + // Clear any pending failure timeouts + failureTimeouts.forEach(clearTimeout); + failureTimeouts = []; + } + }; + + const wrapWithFailureDetection = ( + originalMethod: (...args: unknown[]) => unknown, + methodName: string + ) => { + return function (this: unknown, ...args: unknown[]) { + if (isPosthogBlocked) { + return; + } + + try { + // Set a timeout to detect if the request hangs or fails silently + const timeoutId = setTimeout(() => { + console.warn(`PostHog ${methodName} request timed out`); + handleNetworkFailure(); + }, FAILURE_TIMEOUT); + + failureTimeouts.push(timeoutId); + + // Call original method + const result = originalMethod.apply(this, args); + + // If the method returns a promise, handle success/failure + if (result && typeof result.then === 'function') { + result + .then(() => { + clearTimeout(timeoutId); + const index = failureTimeouts.indexOf(timeoutId); + if (index > -1) failureTimeouts.splice(index, 1); + handleNetworkSuccess(); + }) + .catch((error: unknown) => { + clearTimeout(timeoutId); + const index = failureTimeouts.indexOf(timeoutId); + if (index > -1) failureTimeouts.splice(index, 1); + console.warn(`PostHog ${methodName} failed:`, error); + handleNetworkFailure(); + }); + } else { + // For synchronous methods, assume success and clear timeout + clearTimeout(timeoutId); + const index = failureTimeouts.indexOf(timeoutId); + if (index > -1) failureTimeouts.splice(index, 1); + handleNetworkSuccess(); + } + + return result; + } catch (error) { + console.warn(`PostHog ${methodName} error:`, error); + handleNetworkFailure(); + throw error; + } + }; + }; + + // Override PostHog methods with failure detection + posthog.capture = wrapWithFailureDetection(originalCapture, 'capture'); + posthog.identify = wrapWithFailureDetection(originalIdentify, 'identify'); + posthog.group = wrapWithFailureDetection(originalGroup, 'group'); +}; + +const shutdownPosthog = (posthog: typeof import('posthog-js').default) => { + isPosthogBlocked = true; + + try { + // Clear any pending timeouts + failureTimeouts.forEach(clearTimeout); + failureTimeouts = []; + + // Stop PostHog from making further requests + if (posthog?.reset) { + posthog.reset(); + } + + // Override all methods to be no-ops + const noOp = () => undefined; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + posthog.capture = noOp as any; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + posthog.identify = noOp as any; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + posthog.group = noOp as any; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + posthog.alias = noOp as any; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + posthog.reset = noOp as any; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + posthog.register = noOp as any; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + posthog.unregister = noOp as any; + + console.info('PostHog has been shut down due to network failures (likely ad blocker)'); + } catch (error) { + console.error('Error shutting down PostHog:', error); + } +}; + export const BusterPosthogProvider: React.FC = ({ children }) => { if ((isDev && !DEBUG_POSTHOG) || !POSTHOG_KEY) { return <>{children}; @@ -84,7 +225,7 @@ const PosthogWrapper: React.FC = ({ children }) => { // Initialize PostHog when modules are loaded and user data is available useEffect(() => { - if (POSTHOG_KEY && !isServer && user && posthogModules?.posthog) { + if (POSTHOG_KEY && !isServer && user && posthogModules?.posthog && !isPosthogBlocked) { const { posthog } = posthogModules; if (posthog.__loaded) { @@ -93,6 +234,9 @@ const PosthogWrapper: React.FC = ({ children }) => { posthog.init(POSTHOG_KEY, options); + // Set up failure detection after initialization + setupPosthogFailureDetection(posthog); + const email = user.email; posthog.identify(email, { user,