Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/pages/app/[app].bundle.[bundle].vue
Original file line number Diff line number Diff line change
Expand Up @@ -428,7 +428,7 @@ async function downloadNow() {
})
await dialogStore.onDialogDismiss()
}
openVersion(version.value)
await openVersion(version.value)
}

async function openDownload() {
Expand Down
27 changes: 25 additions & 2 deletions src/services/staleAssetErrors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,16 @@ 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,
]

export function isStaleAssetErrorMessage(message: string | undefined): boolean {
if (!message)
return false
Expand All @@ -30,6 +40,13 @@ export function isKnownCrawlerNoiseErrorMessage(message: string | undefined): bo
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))
}

interface PostHogExceptionLike {
value?: unknown
$exception_value?: unknown
Expand All @@ -49,9 +66,15 @@ export function shouldSuppressPostHogExceptionEvent(event: PostHogEventLike): bo

const exception = event.properties?.$exception_list?.[0]
const exceptionValue = getErrorMessage(exception?.value) ?? getErrorMessage(exception?.$exception_value)
if (isStaleAssetErrorMessage(exceptionValue) || isKnownCrawlerNoiseErrorMessage(exceptionValue))
if (isSuppressibleNoiseErrorMessage(exceptionValue))
return true

const fallbackValue = getErrorMessage(event.properties?.$exception_values?.[0])
return isStaleAssetErrorMessage(fallbackValue) || isKnownCrawlerNoiseErrorMessage(fallbackValue)
return isSuppressibleNoiseErrorMessage(fallbackValue)
}

function isSuppressibleNoiseErrorMessage(message: string | undefined): boolean {
return isStaleAssetErrorMessage(message)
|| isKnownCrawlerNoiseErrorMessage(message)
|| isTransientNetworkErrorMessage(message)
}
8 changes: 7 additions & 1 deletion src/services/supabase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -295,7 +295,13 @@ export async function downloadUrl(provider: string, userId: string, appId: strin
return res.url
}
catch (e) {
throw new Error(`downloadUrl error: ${e instanceof Error ? e.message : String(e)}`)
// A transient browser network drop surfaces as a `TypeError` whose wording
// differs per engine (Chrome "Failed to fetch", WebKit "Load failed",
// Firefox "NetworkError when attempting to fetch resource."). Rethrow it
// untouched so transient-network detection can still recognise the message.
if (e instanceof TypeError)
throw e
throw new Error(`downloadUrl error: ${e instanceof Error ? e.message : String(e)}`, { cause: e })
}
}

Expand Down
17 changes: 14 additions & 3 deletions src/services/versions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,10 +63,21 @@ export async function openVersion(app: Database['public']['Tables']['app_version
const { t } = i18n.global

let signedURL
if (app.r2_path)
signedURL = await downloadUrl(app.storage_provider, app.user_id ?? '', app.app_id, app.id)
else
if (app.r2_path) {
try {
signedURL = await downloadUrl(app.storage_provider, app.user_id ?? '', app.app_id, app.id)
}
catch (error) {
// Transient network failures throw out of `downloadUrl`; surface them in
// the same toast used for the empty-URL case instead of dead-ending.
console.error('Error', error)
toast.error(t('cannot-get-the-test-'))
return
}
}
else {
signedURL = app.external_url
}

if (!signedURL) {
toast.error(t('cannot-get-the-test-'))
Expand Down
34 changes: 33 additions & 1 deletion tests/stale-asset-errors.unit.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { describe, expect, it } from 'vitest'

import { getErrorMessage, isKnownCrawlerNoiseErrorMessage, isStaleAssetErrorMessage, shouldSuppressPostHogExceptionEvent } from '../src/services/staleAssetErrors'
import { getErrorMessage, isKnownCrawlerNoiseErrorMessage, isStaleAssetErrorMessage, isTransientNetworkErrorMessage, shouldSuppressPostHogExceptionEvent } from '../src/services/staleAssetErrors'

describe('stale asset error helpers', () => {
it('matches the stale asset errors currently seen in PostHog', () => {
Expand All @@ -25,6 +25,38 @@ describe('stale asset error helpers', () => {
expect(isKnownCrawlerNoiseErrorMessage('Cannot read properties of null (reading \'save\')')).toBe(false)
})

it('matches the transient browser network failures seen across engines', () => {
expect(isTransientNetworkErrorMessage('Failed to fetch')).toBe(true)
expect(isTransientNetworkErrorMessage('Load failed')).toBe(true)
expect(isTransientNetworkErrorMessage('NetworkError when attempting to fetch resource.')).toBe(true)
// Wrapped by downloadUrl's non-TypeError fallback path
expect(isTransientNetworkErrorMessage('downloadUrl error: NetworkError when attempting to fetch resource.')).toBe(true)
expect(isTransientNetworkErrorMessage('downloadUrl error: Failed to fetch')).toBe(true)
})

it('does not match richer messages that merely start with the same words', () => {
expect(isTransientNetworkErrorMessage('Failed to fetch organization insights')).toBe(false)
expect(isTransientNetworkErrorMessage('Failed to fetch dynamically imported module: https://console.capgo.app/assets/dashboard.js')).toBe(false)
expect(isTransientNetworkErrorMessage('downloadUrl error: HTTP 500')).toBe(false)
expect(isTransientNetworkErrorMessage(undefined)).toBe(false)
})

it('suppresses transient browser network exception events in PostHog', () => {
expect(shouldSuppressPostHogExceptionEvent({
event: '$exception',
properties: {
$exception_list: [{ value: 'Failed to fetch' }],
},
})).toBe(true)

expect(shouldSuppressPostHogExceptionEvent({
event: '$exception',
properties: {
$exception_values: ['downloadUrl error: NetworkError when attempting to fetch resource.'],
},
})).toBe(true)
})

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')
Expand Down
Loading