Files
bsky-social-app/src/logger/sentry/network-errors.test.ts
T
Samuel Newman 34c00e945d trim comments
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-02 18:24:47 +03:00

169 lines
5.1 KiB
TypeScript

import {getMain, XrpcInternalError, XrpcResponseError} from '@atproto/lex'
import {describe, expect, test} from '@jest/globals'
import {type ErrorEvent} from '@sentry/react-native'
import {
dropExpectedNetworkErrors,
isExpectedSentryNetworkError,
} from '#/logger/sentry/network-errors'
import {com} from '#/lexicons'
const method = getMain(com.atproto.server.describeServer)
/** An `XrpcResponseError` for a response that carried no JSON error payload. */
function statusOnlyError(status: number) {
return new XrpcResponseError(method, new Response(null, {status}), undefined)
}
/** A pipethrough `XrpcResponseError`: status rewritten, code forwarded. */
function payloadError(status: number, error: string, message: string) {
const body = {error, message}
return new XrpcResponseError(
method,
new Response(JSON.stringify(body), {
status,
headers: {'content-type': 'application/json'},
}),
{encoding: 'application/json', body},
)
}
function exceptionEvent(type: string, value: string): ErrorEvent {
return {type: undefined, exception: {values: [{type, value}]}}
}
describe('isExpectedSentryNetworkError', () => {
test.each([502, 503, 504])('detects transient upstream status %s', status => {
expect(isExpectedSentryNetworkError(statusOnlyError(status))).toBe(true)
})
test.each([429, 500])('keeps actionable HTTP status %s', status => {
expect(isExpectedSentryNetworkError(statusOnlyError(status))).toBe(false)
})
test('detects a pipethrough 502 forwarding an upstream error code', () => {
const error = payloadError(
502,
'InternalServerError',
'Internal Server Error',
)
expect(error.error).toBe('InternalServerError')
expect(isExpectedSentryNetworkError(error)).toBe(true)
})
test('checks the cause of a wrapper that does not embed it', () => {
expect(
isExpectedSentryNetworkError(
new XrpcInternalError(method, undefined, {
cause: new Error('fetch failed: connection reset'),
}),
),
).toBe(true)
})
test('keeps implementation errors wrapped by XRPC errors', () => {
expect(
isExpectedSentryNetworkError(
new XrpcInternalError(method, undefined, {
cause: new TypeError('URL.canParse is not a function'),
}),
),
).toBe(false)
})
test.each(['UpstreamFailure', 'Upstream Failure', '[UpstreamFailure]'])(
'detects the transient upstream message %s',
message => {
expect(isExpectedSentryNetworkError(message)).toBe(true)
},
)
test.each(['upstreamTimeoutV2', 'handleUpstreamFailureRetry'])(
'keeps the camelCase identifier %s',
message => {
expect(isExpectedSentryNetworkError(message)).toBe(false)
},
)
})
describe('dropExpectedNetworkErrors', () => {
test('drops automatic captures using the original exception', () => {
// the serialized values carry no cause chain, only the hint has the failure
const event = exceptionEvent(
'XrpcInternalError',
'Unable to fulfill XRPC request',
)
expect(
dropExpectedNetworkErrors(event, {
originalException: new XrpcInternalError(method, undefined, {
cause: new Error('fetch failed: connection closed'),
}),
}),
).toBeNull()
})
test('checks every exception in a linked error chain', () => {
const event = {
type: undefined,
exception: {
values: [
{type: 'Error', value: 'Network request failed'},
{
type: 'XrpcInternalError',
value: 'Unable to fulfill XRPC request',
},
],
},
} satisfies ErrorEvent
expect(dropExpectedNetworkErrors(event, {})).toBeNull()
})
test('drops a serialized transient upstream error carrying a payload', () => {
const event = exceptionEvent('XrpcResponseError', 'Upstream Timeout')
expect(dropExpectedNetworkErrors(event, {})).toBeNull()
})
test('drops a serialized transient upstream error with no payload', () => {
const {message} = statusOnlyError(504)
// Locks the text lex builds from the status alone, which the filter matches
expect(message).toBe('Upstream server responded with a 504 error')
expect(
dropExpectedNetworkErrors(
exceptionEvent('XrpcResponseError', message),
{},
),
).toBeNull()
})
test.each([
'[USER REPORT] alice.bsky.social video-upload-aborted',
'[USER REPORT] alice.bsky.social Network request failed',
])('keeps the message event %s', message => {
const event = {type: undefined, message} satisfies ErrorEvent
expect(dropExpectedNetworkErrors(event, {originalException: message})).toBe(
event,
)
})
test('keeps non-network events', () => {
const event = exceptionEvent(
'XrpcFetchError',
'Unexpected fetchHandler() error: URL.canParse is not a function',
)
expect(dropExpectedNetworkErrors(event, {})).toBe(event)
})
test('keeps a camelCase identifier that merely contains an upstream word', () => {
const event = exceptionEvent('Error', 'checkout failed: upstreamTimeoutV2')
expect(dropExpectedNetworkErrors(event, {})).toBe(event)
})
})