src/services/staleAssetErrors.ts
Rama main · réplica de lectura del repositorio original.
import { getErrorMessage } from '~/utils/errors'
export { getErrorMessage } from '~/utils/errors'
const STALE_ASSET_ERROR_PATTERNS = [
/Failed to fetch dynamically imported module/i,
/error loading dynamically imported module/i,
/Importing a module script failed/i,
/Unable to preload CSS/i,
/text\/html.*is not a valid JavaScript MIME type/i,
/Loading chunk [\w-]+ failed/i,
/Loading CSS chunk [\w-]+ failed/i,
]
const KNOWN_CRAWLER_ERROR_PATTERNS = [
/Object Not Found Matching Id:\d+(?:,\s*MethodName:[^,]+,\s*ParamCount:\d+)?/i,
]
// Transient browser network drops surface as a `fetch` TypeError whose exact
// wording differs per engine. Anchored so only the standalone browser message
// (optionally wrapped with a `<context>: ` prefix, e.g. `downloadUrl error: …`)
// is suppressed — not richer messages like "Failed to fetch organization insights".
const TRANSIENT_NETWORK_ERROR_PATTERNS = [
/^(?:.*: )?Failed to fetch$/i,
/^(?:.*: )?Load failed$/i,
/^(?:.*: )?NetworkError when attempting to fetch resource\.?$/i,
]
// vue-router throws this when a lazy route component fails to load (e.g. a stale
// chunk 404 during a deploy). It can also surface if a navigation races the
// automatic reload we trigger for stale chunks, so we treat it as a chunk error.
const COMPONENT_RESOLUTION_ERROR_PATTERNS = [
/Couldn't resolve component/i,
]
export function isStaleAssetErrorMessage(message: string | undefined): boolean {
if (!message)
return false
return STALE_ASSET_ERROR_PATTERNS.some(pattern => pattern.test(message))
}
export function isKnownCrawlerNoiseErrorMessage(message: string | undefined): boolean {
if (!message)
return false
return KNOWN_CRAWLER_ERROR_PATTERNS.some(pattern => pattern.test(message))
}
export function isTransientNetworkErrorMessage(message: string | undefined): boolean {
if (!message)
return false
return TRANSIENT_NETWORK_ERROR_PATTERNS.some(pattern => pattern.test(message))
}
export function isComponentResolutionErrorMessage(message: string | undefined): boolean {
if (!message)
return false
return COMPONENT_RESOLUTION_ERROR_PATTERNS.some(pattern => pattern.test(message))
}
interface PostHogStackFrame {
filename?: unknown
function?: unknown
lineno?: unknown
in_app?: unknown
}
interface PostHogExceptionLike {
value?: unknown
$exception_value?: unknown
stacktrace?: {
frames?: PostHogStackFrame[]
}
}
interface PostHogEventLike {
event?: unknown
properties?: {
$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 API host is local, so a developer running the frontend against
// the remote or preprod API 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 API 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
const cutIndex = url.search(/[?#]/)
return cutIndex === -1 ? url : url.slice(0, cutIndex)
}
// First-party inline script in index.html (theme bootstrap before Vue loads).
const FIRST_PARTY_INLINE_FRAME_FUNCTIONS = new Set([
'applyTheme',
'__setTheme',
])
function isDocumentUrlFrame(frame: PostHogStackFrame, documentUrl: string): boolean {
return stripUrlQueryAndHash(typeof frame.filename === 'string' ? frame.filename : undefined) === documentUrl
}
function isFirstPartyInlineFrame(frame: PostHogStackFrame): boolean {
const func = typeof frame.function === 'string' ? frame.function : ''
return FIRST_PARTY_INLINE_FRAME_FUNCTIONS.has(func)
}
// Console paste, extensions, and AI browser agents typically surface as
// `global code` at line 1 on the page URL. Require that signature so we do not
// drop real errors from our owned inline theme script, which also stacks against
// the document URL but uses normal function names and line numbers.
function hasInjectedCodeFrameSignature(frame: PostHogStackFrame): boolean {
const func = typeof frame.function === 'string' ? frame.function : ''
const lineno = typeof frame.lineno === 'number' ? frame.lineno : undefined
if (func === 'global code' || func === 'eval' || func === 'eval code')
return true
if (lineno === 1 && (func === '' || func === '<anonymous>' || func === 'global code'))
return true
return false
}
// A snippet pasted into the browser console, injected by an extension, or run by
// an AI browser agent surfaces as an $exception whose in-app frames all point at
// the HTML document with a console/eval signature. Bundled app code runs from
// hashed chunks under `/assets/`; our inline theme bootstrap is allowlisted.
export function isInjectedDocumentCodeException(exception: PostHogExceptionLike | undefined, currentUrl: unknown): boolean {
const documentUrl = stripUrlQueryAndHash(typeof currentUrl === 'string' ? currentUrl : undefined)
if (!documentUrl)
return false
const frames = exception?.stacktrace?.frames
if (!Array.isArray(frames))
return false
const inAppFrames = frames.filter(frame => frame?.in_app === true)
if (inAppFrames.length === 0)
return false
if (!inAppFrames.every(frame => isDocumentUrlFrame(frame, documentUrl)))
return false
if (inAppFrames.some(frame => isFirstPartyInlineFrame(frame)))
return false
return inAppFrames.some(frame => hasInjectedCodeFrameSignature(frame))
}
export function shouldSuppressPostHogExceptionEvent(event: PostHogEventLike): boolean {
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))
return true
const fallbackValue = getErrorMessage(event.properties?.$exception_values?.[0])
if (isSuppressibleNoiseErrorMessage(fallbackValue))
return true
return isInjectedDocumentCodeException(exception, event.properties?.$current_url)
}
function isSuppressibleNoiseErrorMessage(message: string | undefined): boolean {
return isStaleAssetErrorMessage(message)
|| isComponentResolutionErrorMessage(message)
|| isKnownCrawlerNoiseErrorMessage(message)
|| isTransientNetworkErrorMessage(message)
}