Skip to content

Commit 7adc8a3

Browse files
committed
fix(realtime): ignore token failures from a superseded handshake and report persistent token outages
1 parent ccd81ba commit 7adc8a3

1 file changed

Lines changed: 55 additions & 40 deletions

File tree

‎apps/sim/app/workspace/providers/socket-provider.tsx‎

Lines changed: 55 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import type {
2525
WorkflowRevertedBroadcast,
2626
WorkflowUpdatedBroadcast,
2727
} from '@sim/realtime-protocol/events'
28+
import { getErrorMessage } from '@sim/utils/errors'
2829
import { generateId } from '@sim/utils/id'
2930
import { randomFloat } from '@sim/utils/random'
3031
import { backoffWithJitter, parseRetryAfter } from '@sim/utils/retry'
@@ -430,6 +431,51 @@ export function SocketProvider({ children, user }: SocketProviderProps) {
430431
timestamp: new Date().toISOString(),
431432
})
432433

434+
/**
435+
* Reports a realtime connection that is not coming back.
436+
*
437+
* A socket that never connects raises no exception anywhere — every
438+
* failure path here is handled, so error tracking sees nothing and the
439+
* only user-visible trace is a "Reconnecting…" toast. Socket.IO then
440+
* retries the same URL forever, so the failure is both permanent and
441+
* silent. This is the one signal that distinguishes "the realtime
442+
* service is down" from "this client resolved the wrong URL", which is
443+
* why the origin is reported alongside the reason.
444+
*
445+
* Fires at most once per socket instance, at the point the toast
446+
* appears, rather than once per retry.
447+
*
448+
* Called once per failed attempt: from `connect_error`, and from the auth
449+
* callback when it ends an attempt itself because no token could be minted
450+
* (that attempt never reaches `connect_error`). `connect_error` is the only
451+
* socket handler that sees exactly one event per attempt: a failed
452+
* *reconnect* also emits the manager's `reconnect_error` (`manager.reconnect()` calls
453+
* `open()`, whose error path emits `error` — which the socket re-emits as
454+
* `connect_error` — and then emits `reconnect_error` itself), so counting
455+
* in both would advance twice per try and trip the threshold early.
456+
*/
457+
const reportPersistentConnectFailure = (reason: string) => {
458+
connectFailureCountRef.current += 1
459+
if (
460+
connectFailureReportedRef.current ||
461+
connectFailureCountRef.current < CONNECT_FAILURES_BEFORE_REPORT
462+
) {
463+
return
464+
}
465+
connectFailureReportedRef.current = true
466+
467+
captureClientEvent('realtime_connection_failing', {
468+
socket_origin: socketOrigin,
469+
expected_socket_origin_configured: Boolean(getEnv('NEXT_PUBLIC_SOCKET_URL')?.trim()),
470+
attempts: connectFailureCountRef.current,
471+
reason,
472+
})
473+
}
474+
475+
// Each auth callback owns one handshake; a token request that outlives it must
476+
// not end the attempt that replaced it.
477+
let authAttemptGeneration = 0
478+
433479
const socketInstance = io(socketUrl, {
434480
transports: ['websocket', 'polling'],
435481
withCredentials: true,
@@ -442,6 +488,7 @@ export function SocketProvider({ children, user }: SocketProviderProps) {
442488
// otherwise one early 401 would still be set when a later, unrelated
443489
// denial exhausts the retry budget.
444490
sessionRejectedRef.current = false
491+
const attempt = ++authAttemptGeneration
445492
try {
446493
const freshToken = await generateSocketToken()
447494
cb({ token: freshToken })
@@ -453,7 +500,13 @@ export function SocketProvider({ children, user }: SocketProviderProps) {
453500
cb({ token: null })
454501
return
455502
}
456-
if (!socketInstance.active || socketInstance.connected) return
503+
if (
504+
attempt !== authAttemptGeneration ||
505+
!socketInstance.active ||
506+
socketInstance.connected
507+
) {
508+
return
509+
}
457510

458511
// Withholding `cb` would park the transport until the server's 45 s connect
459512
// timeout, and the Manager resets its backoff on every open, so pace it here.
@@ -477,6 +530,7 @@ export function SocketProvider({ children, user }: SocketProviderProps) {
477530
setIsConnecting(false)
478531
setCurrentSocketId(null)
479532
setIsReconnecting(true)
533+
reportPersistentConnectFailure(getErrorMessage(error))
480534
logger.warn('Socket token unavailable, reconnecting after delay', {
481535
attempt: tokenRetryAttemptsRef.current,
482536
delayMs: Math.round(delayMs),
@@ -490,45 +544,6 @@ export function SocketProvider({ children, user }: SocketProviderProps) {
490544
},
491545
})
492546

493-
/**
494-
* Reports a realtime connection that is not coming back.
495-
*
496-
* A socket that never connects raises no exception anywhere — every
497-
* failure path here is handled, so error tracking sees nothing and the
498-
* only user-visible trace is a "Reconnecting…" toast. Socket.IO then
499-
* retries the same URL forever, so the failure is both permanent and
500-
* silent. This is the one signal that distinguishes "the realtime
501-
* service is down" from "this client resolved the wrong URL", which is
502-
* why the origin is reported alongside the reason.
503-
*
504-
* Fires at most once per socket instance, at the point the toast
505-
* appears, rather than once per retry.
506-
*
507-
* Called from `connect_error` and nowhere else. That is the only handler
508-
* that sees exactly one event per attempt: a failed *reconnect* also
509-
* emits the manager's `reconnect_error` (`manager.reconnect()` calls
510-
* `open()`, whose error path emits `error` — which the socket re-emits as
511-
* `connect_error` — and then emits `reconnect_error` itself), so counting
512-
* in both would advance twice per try and trip the threshold early.
513-
*/
514-
const reportPersistentConnectFailure = (reason: string) => {
515-
connectFailureCountRef.current += 1
516-
if (
517-
connectFailureReportedRef.current ||
518-
connectFailureCountRef.current < CONNECT_FAILURES_BEFORE_REPORT
519-
) {
520-
return
521-
}
522-
connectFailureReportedRef.current = true
523-
524-
captureClientEvent('realtime_connection_failing', {
525-
socket_origin: socketOrigin,
526-
expected_socket_origin_configured: Boolean(getEnv('NEXT_PUBLIC_SOCKET_URL')?.trim()),
527-
attempts: connectFailureCountRef.current,
528-
reason,
529-
})
530-
}
531-
532547
socketInstance.on('connect', () => {
533548
setIsConnected(true)
534549
setIsConnecting(false)

0 commit comments

Comments
 (0)