From 13f6a34758c5fdbc9cce449ed53fb4e98d9fc217 Mon Sep 17 00:00:00 2001 From: "posthog-eu[bot]" <226701856+posthog-eu[bot]@users.noreply.github.com> Date: Thu, 17 Sep 2026 12:00:45 +0000 Subject: [PATCH] fix(analytics): suppress exception events from local dev hosts The PostHog capture gate skips analytics only when the Supabase host is local. A developer running the frontend against remote or preprod Supabase passes that gate, so their transient compile and hot-reload errors reach production error tracking and open new issues on every refactor. Extend the before_send suppression hook to drop $exception events whose page host is a local dev host (localhost, 127.0.0.1, *.local), read from $host and from $current_url. Add unit tests for the new isLocalDevHost helper and the host-based suppression path. Generated-By: PostHog Desktop Task-Id: 86f1c693-158f-4d23-9908-726a591aa262 --- src/services/staleAssetErrors.ts | 35 +++++++++++++++++++ tests/stale-asset-errors.unit.test.ts | 48 ++++++++++++++++++++++++++- 2 files changed, 82 insertions(+), 1 deletion(-) diff --git a/src/services/staleAssetErrors.ts b/src/services/staleAssetErrors.ts index 8a46c54df9..49cf7180b0 100644 --- a/src/services/staleAssetErrors.ts +++ b/src/services/staleAssetErrors.ts @@ -82,9 +82,41 @@ interface PostHogEventLike { $exception_list?: PostHogExceptionLike[] $exception_values?: unknown[] $current_url?: unknown + $host?: unknown } } +// Local dev-server hosts. The capture gate in posthog.ts skips analytics only +// when the Supabase host is local, so a developer running the frontend against +// remote or preprod Supabase still sends events. Their transient compile and +// hot-reload errors then open production error-tracking issues. Match the page +// host to drop those events regardless of the Supabase host. +const LOCAL_DEV_HOST_PATTERNS = [ + /^localhost$/i, + /^127\.0\.0\.1$/, + /\.local$/i, +] + +function hostFromUrl(url: unknown): string | undefined { + if (typeof url !== 'string' || url === '') + return undefined + + try { + return new URL(url).host + } + catch { + return undefined + } +} + +export function isLocalDevHost(host: unknown): boolean { + if (typeof host !== 'string' || host === '') + return false + + const hostname = host.replace(/:\d+$/, '') + return LOCAL_DEV_HOST_PATTERNS.some(pattern => pattern.test(hostname)) +} + function stripUrlQueryAndHash(url: string | undefined): string | undefined { if (!url) return undefined @@ -155,6 +187,9 @@ export function shouldSuppressPostHogExceptionEvent(event: PostHogEventLike): bo if (event.event !== '$exception') return false + if (isLocalDevHost(event.properties?.$host) || isLocalDevHost(hostFromUrl(event.properties?.$current_url))) + return true + const exception = event.properties?.$exception_list?.[0] const exceptionValue = getErrorMessage(exception?.value) ?? getErrorMessage(exception?.$exception_value) if (isSuppressibleNoiseErrorMessage(exceptionValue)) diff --git a/tests/stale-asset-errors.unit.test.ts b/tests/stale-asset-errors.unit.test.ts index bde40f4077..4c32f25cfc 100644 --- a/tests/stale-asset-errors.unit.test.ts +++ b/tests/stale-asset-errors.unit.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest' -import { getErrorMessage, isComponentResolutionErrorMessage, isInjectedDocumentCodeException, isKnownCrawlerNoiseErrorMessage, isStaleAssetErrorMessage, isTransientNetworkErrorMessage, shouldSuppressPostHogExceptionEvent } from '../src/services/staleAssetErrors' +import { getErrorMessage, isComponentResolutionErrorMessage, isInjectedDocumentCodeException, isKnownCrawlerNoiseErrorMessage, isLocalDevHost, isStaleAssetErrorMessage, isTransientNetworkErrorMessage, shouldSuppressPostHogExceptionEvent } from '../src/services/staleAssetErrors' describe('stale asset error helpers', () => { it('matches the stale asset errors currently seen in PostHog', () => { @@ -151,6 +151,52 @@ describe('stale asset error helpers', () => { })).toBe(false) }) + it('matches local dev-server hosts regardless of port', () => { + expect(isLocalDevHost('localhost:5175')).toBe(true) + expect(isLocalDevHost('localhost')).toBe(true) + expect(isLocalDevHost('127.0.0.1:5173')).toBe(true) + expect(isLocalDevHost('capgo.local')).toBe(true) + expect(isLocalDevHost('capgo.local:3000')).toBe(true) + }) + + it('does not match production hosts or malformed values', () => { + expect(isLocalDevHost('console.capgo.app')).toBe(false) + expect(isLocalDevHost('capgo.app:443')).toBe(false) + expect(isLocalDevHost('notlocalhost.com')).toBe(false) + expect(isLocalDevHost('')).toBe(false) + expect(isLocalDevHost(undefined)).toBe(false) + }) + + it('suppresses dev-server exception events by page host', () => { + // The $host property carries the dev-server host with its port + expect(shouldSuppressPostHogExceptionEvent({ + event: '$exception', + properties: { + $host: 'localhost:5175', + $exception_list: [{ value: 'ReferenceError: disableRollout is not defined' }], + }, + })).toBe(true) + + // Falls back to the host parsed from $current_url when $host is absent + expect(shouldSuppressPostHogExceptionEvent({ + event: '$exception', + properties: { + $current_url: 'http://localhost:5175/app/1234/channel/production', + $exception_list: [{ value: 'ReferenceError: disableRollout is not defined' }], + }, + })).toBe(true) + + // The same exception from the production host is kept + expect(shouldSuppressPostHogExceptionEvent({ + event: '$exception', + properties: { + $host: 'console.capgo.app', + $current_url: 'https://console.capgo.app/app/1234/channel/production', + $exception_list: [{ value: 'ReferenceError: disableRollout is not defined' }], + }, + })).toBe(false) + }) + it('extracts useful messages from arbitrary rejection values', () => { expect(getErrorMessage(new Error('Importing a module script failed.'))).toBe('Importing a module script failed.') expect(getErrorMessage({ message: 'Unable to preload CSS for /assets/main.css' })).toBe('Unable to preload CSS for /assets/main.css')