@@ -163,6 +163,12 @@ class GameClient {
163163 private connection : ConnectionManager ;
164164 private turnTimer ! : TurnTimerManager ;
165165
166+ // Phase timing for telemetry
167+ private phaseStartedAt : number | null = null ;
168+ private turnStartedAt : number | null = null ;
169+ private phaseDurations : Record < string , number > = { } ;
170+ private lastTurnNumber = - 1 ;
171+
166172 // Presentation orchestration deps
167173 // (lazy — renderer/ui available after constructor)
168174 private _presentationDeps : PresentationDeps | null = null ;
@@ -284,6 +290,7 @@ class GameClient {
284290 ) ;
285291 this . ui = new UIManager ( ) ;
286292 this . tutorial = new Tutorial ( ) ;
293+ this . tutorial . onTelemetry = ( evt ) => track ( evt ) ;
287294 this . tooltipEl = byId ( 'shipTooltip' ) ;
288295
289296 this . connection = createConnectionManager ( {
@@ -393,8 +400,16 @@ class GameClient {
393400 }
394401
395402 private setState ( newState : ClientState ) {
403+ const prevState = this . ctx . state ;
396404 this . ctx . state = newState ;
397405
406+ // Accumulate phase duration for telemetry
407+ this . recordPhaseDuration ( prevState ) ;
408+
409+ if ( newState . startsWith ( 'playing_' ) ) {
410+ this . phaseStartedAt = Date . now ( ) ;
411+ }
412+
398413 // Hide tooltip on state changes
399414 hide ( this . tooltipEl ) ;
400415
@@ -568,6 +583,10 @@ class GameClient {
568583 this . ctx . scenario = scenario ;
569584 this . ctx . playerId = 0 ;
570585 this . lastLoggedTurn = - 1 ;
586+ this . lastTurnNumber = - 1 ;
587+ this . turnStartedAt = null ;
588+ this . phaseStartedAt = null ;
589+ this . phaseDurations = { } ;
571590 this . renderer . setPlayerId ( 0 ) ;
572591 this . ctx . transport = this . createLocalTransport ( ) ;
573592
@@ -851,6 +870,10 @@ class GameClient {
851870 this . dispatch ( { type : 'exitToMenu' } ) ;
852871 return ;
853872
873+ case 'backToMenu' :
874+ track ( 'scenario_browsed' ) ;
875+ return ;
876+
854877 case 'selectShip' :
855878 this . dispatch ( {
856879 type : 'selectShip' ,
@@ -1092,6 +1115,37 @@ class GameClient {
10921115 }
10931116 }
10941117
1118+ // --- Phase timing telemetry ---
1119+
1120+ private recordPhaseDuration ( prevState : ClientState ) {
1121+ if ( this . phaseStartedAt !== null && prevState . startsWith ( 'playing_' ) ) {
1122+ const phase = prevState . replace ( 'playing_' , '' ) ;
1123+ // Skip non-player phases
1124+ if ( phase !== 'opponentTurn' && phase !== 'movementAnim' ) {
1125+ const elapsed = Date . now ( ) - this . phaseStartedAt ;
1126+ this . phaseDurations [ phase ] =
1127+ ( this . phaseDurations [ phase ] ?? 0 ) + elapsed ;
1128+ }
1129+ this . phaseStartedAt = null ;
1130+ }
1131+ }
1132+
1133+ private emitTurnCompleted ( ) {
1134+ if ( this . turnStartedAt === null ) return ;
1135+
1136+ const totalMs = Date . now ( ) - this . turnStartedAt ;
1137+ track ( 'turn_completed' , {
1138+ turn : this . lastTurnNumber ,
1139+ totalMs,
1140+ phases : { ...this . phaseDurations } ,
1141+ scenario : this . ctx . scenario ,
1142+ mode : this . ctx . isLocalGame ? 'local' : 'multiplayer' ,
1143+ } ) ;
1144+
1145+ this . phaseDurations = { } ;
1146+ this . turnStartedAt = Date . now ( ) ;
1147+ }
1148+
10951149 // --- Game actions ---
10961150
10971151 private onAnimationComplete ( ) {
@@ -1113,6 +1167,15 @@ class GameClient {
11131167 ) ;
11141168
11151169 if ( transition . turnLogNumber !== null && transition . turnLogPlayerLabel ) {
1170+ // Emit turn_completed for the previous turn
1171+ if ( this . lastTurnNumber > 0 ) {
1172+ this . emitTurnCompleted ( ) ;
1173+ } else {
1174+ // First turn — just start timing
1175+ this . turnStartedAt = Date . now ( ) ;
1176+ }
1177+
1178+ this . lastTurnNumber = transition . turnLogNumber ;
11161179 this . lastLoggedTurn = transition . turnLogNumber ;
11171180 this . ui . logTurn ( transition . turnLogNumber , transition . turnLogPlayerLabel ) ;
11181181 }
0 commit comments