Files
accounted/lib/observability/__tests__/is-client-disconnect.test.ts
T
Jakob Wennberg b1f7116231 fix(observability): clear the dead session on the first bounce, stop filing client disconnects as exceptions, scope the Hem probe (#2114)
Three small defects found underneath error clusters that are themselves benign.

bounceToAuth returned a bare NextResponse.redirect and never copied the cookies
off supabaseResponse, so the Set-Cookie headers that clear a dead session were
thrown away on the first bounce and the browser replayed the dead refresh token
once more on /login. That doubled both the GoTrue 400s and the log volume. The
AuthApiError itself is left alone: it is correct session-expiry handling that
auth-js logs from inside node_modules, and getUser() returns it as a value.

"The destination stream closed early." is a client disconnecting mid-stream,
produced inside React's Flight server. Next's own isAbortError filter does not
recognise React's cancel error, so instrumentation.ts reported it to PostHog
Error Tracking as a real exception against real users' session replays and paid
an awaited flush on an otherwise-healthy request. A narrow predicate now
early-returns before PostHog is touched. This cannot remove the line from
Vercel's runtime-error table, which is fed by Next's stderr.

other-account-hint.ts issued an unfiltered journal_entries probe on the
blocking Hem render path, inside a render Promise.all: roughly 1 in 20 Hem
loads waited an extra 2.5 s for an advisory nudge. An unfiltered probe on a
multi-tenant table is also a correctness smell. It is now company-scoped and
off the blocking path.


Claude-Session: https://claude.ai/code/session_016ifKg6Ec67A39oxfGPU1yc

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-01 15:03:23 +02:00

56 lines
2.1 KiB
TypeScript

import { describe, it, expect } from 'vitest'
import { isClientDisconnectError } from '../is-client-disconnect'
describe('isClientDisconnectError', () => {
it('matches React\'s cancel error for a stream the client closed', () => {
expect(isClientDisconnectError(new Error('The destination stream closed early.'))).toBe(true)
})
it('matches the sibling cancel error React raises on a write failure', () => {
expect(
isClientDisconnectError(new Error('The destination stream errored while writing data.')),
).toBe(true)
})
it('matches Next\'s own ResponseAborted error by name', () => {
const aborted = Object.assign(new Error('The response was aborted'), {
name: 'ResponseAborted',
})
expect(isClientDisconnectError(aborted)).toBe(true)
})
it('does not match an ordinary error', () => {
expect(isClientDisconnectError(new Error('boom'))).toBe(false)
expect(isClientDisconnectError(new TypeError('fetch failed'))).toBe(false)
})
it('does not match a real provider timeout or socket reset', () => {
// AbortSignal.timeout throws this shape all over lib/providers; treating
// it as a client disconnect would hide genuine integration failures.
expect(isClientDisconnectError(new DOMException('Aborted', 'AbortError'))).toBe(false)
expect(
isClientDisconnectError(Object.assign(new Error('socket hang up'), { code: 'ECONNRESET' })),
).toBe(false)
expect(
isClientDisconnectError(
Object.assign(new Error('Premature close'), { code: 'ERR_STREAM_PREMATURE_CLOSE' }),
),
).toBe(false)
})
it('matches the message exactly, so a real error cannot hide behind the phrase', () => {
expect(
isClientDisconnectError(
new Error('Supabase query failed: The destination stream closed early.'),
),
).toBe(false)
})
it('is false for anything that is not an error object', () => {
expect(isClientDisconnectError(null)).toBe(false)
expect(isClientDisconnectError(undefined)).toBe(false)
expect(isClientDisconnectError('The destination stream closed early.')).toBe(false)
expect(isClientDisconnectError(42)).toBe(false)
})
})