@@ -7,14 +7,18 @@ import { useOnPageExit } from '@src/hooks/network/useOnPageExit'
77import { useOnPageIdle } from '@src/hooks/network/useOnPageIdle'
88import { useOnPageMounted } from '@src/hooks/network/useOnPageMounted'
99import { useOnPageResize } from '@src/hooks/network/useOnPageResize'
10- import { useOnPeerConnectionClose } from '@src/hooks/network/useOnPeerConnectionClose'
10+ import {
11+ type EngineDisconnectEvent ,
12+ useOnPeerConnectionClose ,
13+ } from '@src/hooks/network/useOnPeerConnectionClose'
1114import { useOnVitestEngineOnline } from '@src/hooks/network/useOnVitestEngineOnline'
1215import { useOnWebsocketClose } from '@src/hooks/network/useOnWebsocketClose'
1316import { useOnWindowOnlineOffline } from '@src/hooks/network/useOnWindowOnlineOffline'
1417import { useTryConnect } from '@src/hooks/network/useTryConnect'
1518import { useModelingContext } from '@src/hooks/useModelingContext'
1619import { useNetworkContext } from '@src/hooks/useNetworkContext'
1720import { NetworkHealthState } from '@src/hooks/useNetworkStatus'
21+ import { ClientErrorCode , reportClientError } from '@src/lib/clientErrors'
1822import { findOperationForArtifact } from '@src/lang/queryAst'
1923import {
2024 getArtifactOfTypes ,
@@ -32,6 +36,7 @@ import {
3236} from '@src/lib/selections'
3337import { Themes , getResolvedTheme } from '@src/lib/theme'
3438import { err , reportRejection } from '@src/lib/trap'
39+ import { EngineCommandManagerEvents } from '@src/network/utils'
3540import type {
3641 EngineSceneExtensionContext ,
3742 EngineSceneStreamLayer ,
@@ -41,6 +46,14 @@ import { use, useCallback, useMemo, useRef, useState } from 'react'
4146
4247const TIME_TO_CONNECT = 30_000
4348
49+ const stringHash = ( value : string ) => {
50+ let hash = 0
51+ for ( let i = 0 ; i < value . length ; i ++ ) {
52+ hash = ( hash * 31 + value . charCodeAt ( i ) ) | 0
53+ }
54+ return hash . toString ( 36 )
55+ }
56+
4457interface ConnectionStreamProps {
4558 authToken : string | undefined
4659 sketchSolveStreamDimming ?: number
@@ -81,6 +94,45 @@ export const ConnectionStream = (props: ConnectionStreamProps) => {
8194 return isSafari ? ' object-fill' : ''
8295 } , [ ] )
8396
97+ const reportEngineDisconnect = useCallback (
98+ ( eventType : EngineDisconnectEvent , extra ?: Record < string , unknown > ) => {
99+ const kclSource = kclManager . code
100+ const connection = engineCommandManager . connection
101+
102+ void reportClientError ( {
103+ code : ClientErrorCode . EngineDisconnect ,
104+ message : `Engine disconnected: ${ eventType } ` ,
105+ dedupeKey : `ConnectionStream:engine-disconnect:${ eventType } :${ kclManager . currentFileName ?? 'unknown' } :${ stringHash ( kclSource ) } ` ,
106+ extra : {
107+ source : 'ConnectionStream' ,
108+ eventType,
109+ projectName : project ?. name ,
110+ currentFileName : kclManager . currentFileName ,
111+ isSceneReady,
112+ numberOfConnectionAttempts,
113+ pendingCommandCount : Object . keys ( engineCommandManager . pendingCommands )
114+ . length ,
115+ hasConnection : Boolean ( connection ) ,
116+ connectionId : connection ?. id ,
117+ connectionConnected : connection ?. connected ,
118+ peerConnectionState : connection ?. peerConnection ?. connectionState ,
119+ iceConnectionState : connection ?. peerConnection ?. iceConnectionState ,
120+ dataChannelReadyState : connection ?. unreliableDataChannel ?. readyState ,
121+ ...extra ,
122+ kclSourceLength : kclSource . length ,
123+ kclSource,
124+ } ,
125+ } )
126+ } ,
127+ [
128+ engineCommandManager ,
129+ isSceneReady ,
130+ kclManager ,
131+ numberOfConnectionAttempts ,
132+ project ?. name ,
133+ ]
134+ )
135+
84136 const handleMouseUp : MouseEventHandler < HTMLDivElement > = useCallback (
85137 ( e ) => {
86138 if ( ! isNetworkOkay ) return
@@ -337,7 +389,10 @@ export const ConnectionStream = (props: ConnectionStreamProps) => {
337389
338390 const onWebSocketCloseParams = useMemo (
339391 ( ) => ( {
340- callback : ( ) => {
392+ callback : ( code : string | undefined ) => {
393+ reportEngineDisconnect ( EngineCommandManagerEvents . WebsocketClosed , {
394+ websocketCloseCode : code ,
395+ } )
341396 setShowManualConnect ( false )
342397 tryConnecting ( {
343398 authToken : props . authToken || '' ,
@@ -356,13 +411,22 @@ export const ConnectionStream = (props: ConnectionStreamProps) => {
356411 setShowManualConnect ( true )
357412 } )
358413 } ,
359- infiniteDetectionLoopCallback : ( ) => {
414+ infiniteDetectionLoopCallback : ( code : string | undefined ) => {
415+ reportEngineDisconnect ( EngineCommandManagerEvents . WebsocketClosed , {
416+ websocketCloseCode : code ,
417+ } )
360418 setShowManualConnect ( true )
361419 } ,
362420 engineCommandManager,
363421 } ) ,
364422 // eslint-disable-next-line react-hooks/exhaustive-deps
365- [ isConnecting , numberOfConnectionAttempts , props . authToken , settings ]
423+ [
424+ isConnecting ,
425+ numberOfConnectionAttempts ,
426+ props . authToken ,
427+ reportEngineDisconnect ,
428+ settings ,
429+ ]
366430 )
367431 useOnWebsocketClose ( onWebSocketCloseParams )
368432
@@ -396,7 +460,8 @@ export const ConnectionStream = (props: ConnectionStreamProps) => {
396460
397461 const onPeerConnectionCloseParams = useMemo (
398462 ( ) => ( {
399- callback : ( ) => {
463+ callback : ( eventType : EngineDisconnectEvent ) => {
464+ reportEngineDisconnect ( eventType )
400465 setShowManualConnect ( false )
401466 tryConnecting ( {
402467 authToken : props . authToken || '' ,
@@ -418,7 +483,13 @@ export const ConnectionStream = (props: ConnectionStreamProps) => {
418483 engineCommandManager,
419484 } ) ,
420485 // eslint-disable-next-line react-hooks/exhaustive-deps
421- [ isConnecting , numberOfConnectionAttempts , props . authToken , settings ]
486+ [
487+ isConnecting ,
488+ numberOfConnectionAttempts ,
489+ props . authToken ,
490+ reportEngineDisconnect ,
491+ settings ,
492+ ]
422493 )
423494 useOnPeerConnectionClose ( onPeerConnectionCloseParams )
424495
0 commit comments