@@ -5,6 +5,7 @@ import { PointsService } from '../services/points';
55import { IDService } from '../services/id' ;
66import { DivisionService } from '../services/divisions' ;
77import { DatabaseContextFactory } from '../services/database-context' ;
8+ import { PostHogService } from '../services/posthog' ;
89
910const MAX_STATE_SIZE = 1000000 ; // 1MB limit for persisted payloads
1011
@@ -105,6 +106,7 @@ export class Connection {
105106 pilots : 0 ,
106107 observers : 0 ,
107108 } ;
109+ private posthog : PostHogService ;
108110 private lastKnownAirport = 'unknown' ;
109111
110112 constructor (
@@ -114,6 +116,7 @@ export class Connection {
114116 private state : DurableObjectState ,
115117 ) {
116118 this . objectId = state . id . toString ( ) ;
119+ this . posthog = new PostHogService ( env ) ;
117120 this . loadPersistedState ( ) ;
118121 }
119122
@@ -162,6 +165,20 @@ export class Connection {
162165 }
163166 }
164167
168+ private emitAnalytics ( event : string , properties : Record < string , unknown > ) {
169+ const filtered : Record < string , unknown > = { } ;
170+ for ( const [ key , value ] of Object . entries ( properties ) ) {
171+ if ( value !== undefined ) {
172+ filtered [ key ] = value ;
173+ }
174+ }
175+ try {
176+ this . posthog . track ( event , filtered ) ;
177+ } catch {
178+ // ignore analytics failures
179+ }
180+ }
181+
165182 private deserializeAirportState ( airport : string , stored : unknown ) : AirportState {
166183 const airportState = ( stored || { } ) as {
167184 objects ?: Record < string , { id : string ; state : unknown ; controllerId ?: string ; timestamp : number } > ;
@@ -499,7 +516,7 @@ export class Connection {
499516 }
500517 const removed = this . unregisterSocket ( socket ) ;
501518 if ( removed ) {
502- await this . trackDisconnection ( ) ;
519+ await this . trackDisconnection ( removed , 'banned' ) ;
503520 }
504521 socket . close ( 1008 , 'Banned' ) ;
505522 clearInterval ( interval ) ;
@@ -529,7 +546,7 @@ export class Connection {
529546
530547 const removed = this . unregisterSocket ( socket ) ;
531548 if ( removed ) {
532- await this . trackDisconnection ( ) ;
549+ await this . trackDisconnection ( removed , 'vatsim_offline' ) ;
533550 }
534551 socket . close ( 1000 , 'No longer connected to VATSIM' ) ;
535552 clearInterval ( interval ) ;
@@ -560,7 +577,7 @@ export class Connection {
560577
561578 const removed = this . unregisterSocket ( socket ) ;
562579 if ( removed ) {
563- await this . trackDisconnection ( ) ;
580+ await this . trackDisconnection ( removed , 'role_changed' ) ;
564581 }
565582 socket . close ( 1000 , 'Role changed on VATSIM' ) ;
566583 clearInterval ( interval ) ;
@@ -704,7 +721,7 @@ export class Connection {
704721 this . startHeartbeat ( server ) ;
705722
706723 // Track connection in background to avoid blocking WS upgrade on slow D1
707- this . trackConnection ( clientType ) . catch ( ( err ) => {
724+ this . trackConnection ( clientType , airport ) . catch ( ( err ) => {
708725 console . error ( 'trackConnection failed:' , err ) ;
709726 } ) ;
710727
@@ -816,7 +833,7 @@ export class Connection {
816833 ) ;
817834 const removed = this . unregisterSocket ( server ) ;
818835 if ( removed ) {
819- await this . trackDisconnection ( ) ;
836+ await this . trackDisconnection ( removed , 'banned' ) ;
820837 }
821838 server . close ( 1008 , 'Banned' ) ;
822839 return ;
@@ -858,7 +875,14 @@ export class Connection {
858875 } ;
859876
860877 await this . broadcastToControllers ( broadcastPacket , server ) ;
861- await this . trackMessage ( clientType ) ;
878+ this . trackMessage ( {
879+ clientType,
880+ messageType : 'STOPBAR_CROSSING' ,
881+ airport,
882+ meta : {
883+ objectId,
884+ } ,
885+ } ) ;
862886 break ;
863887 }
864888
@@ -913,7 +937,21 @@ export class Connection {
913937 timestamp,
914938 } ;
915939 await this . broadcast ( broadcastPacket , server ) ;
916- await this . trackMessage ( clientType ) ;
940+ const data = ( ( packet as Packet ) . data || { } ) as Record < string , unknown > ;
941+ const patchValue = data . patch as unknown ;
942+ const meta : Record < string , unknown > = {
943+ objectId : typeof data . objectId === 'string' ? data . objectId : undefined ,
944+ updateMode : patchValue !== undefined ? 'patch' : 'state' ,
945+ } ;
946+ if ( patchValue && typeof patchValue === 'object' && ! Array . isArray ( patchValue ) ) {
947+ meta . patchKeys = Object . keys ( patchValue as Record < string , unknown > ) . length ;
948+ }
949+ this . trackMessage ( {
950+ clientType,
951+ messageType : 'STATE_UPDATE' ,
952+ airport : socketInfo . airport ,
953+ meta,
954+ } ) ;
917955 } catch ( updateError ) {
918956 throw new Error (
919957 `State update failed: ${ updateError instanceof Error ? updateError . message : String ( updateError ) } ` ,
@@ -928,7 +966,7 @@ export class Connection {
928966 }
929967 const removed = this . unregisterSocket ( server ) ;
930968 if ( removed ) {
931- await this . trackDisconnection ( ) ;
969+ await this . trackDisconnection ( removed , 'client_close' ) ;
932970 }
933971 server . close ( 1000 , 'Client requested disconnection' ) ;
934972 break ;
@@ -988,7 +1026,7 @@ export class Connection {
9881026 return ;
9891027 }
9901028 try {
991- await this . trackDisconnection ( ) ;
1029+ await this . trackDisconnection ( removed , 'close_event' ) ;
9921030 } catch ( e ) {
9931031 console . warn ( 'trackDisconnection failed on close (non-fatal):' , e ) ;
9941032 }
@@ -1008,7 +1046,7 @@ export class Connection {
10081046 return ;
10091047 }
10101048 try {
1011- await this . trackDisconnection ( ) ;
1049+ await this . trackDisconnection ( removed , 'socket_error' ) ;
10121050 } catch ( e ) {
10131051 console . warn ( 'trackDisconnection failed on error (non-fatal):' , e ) ;
10141052 }
@@ -1017,8 +1055,7 @@ export class Connection {
10171055 return new Response ( null , { status : 101 , webSocket : client } ) ;
10181056 }
10191057
1020- private async trackConnection ( _clientType : ClientType ) {
1021- void _clientType ;
1058+ private async trackConnection ( clientType : ClientType , airport : string ) {
10221059 await this . updateActiveConnections ( 1 ) ;
10231060
10241061 // Add this object to active_objects table when first connection is made
@@ -1034,9 +1071,19 @@ export class Connection {
10341071 console . warn ( 'Failed to upsert active_objects on connect (non-fatal):' , e instanceof Error ? e . message : e ) ;
10351072 }
10361073 }
1074+
1075+ this . emitAnalytics ( 'ws_connection_opened' , {
1076+ airport,
1077+ clientType,
1078+ socket_count : this . sockets . size ,
1079+ object_id : this . objectId ,
1080+ controllers_online : this . connectionCounts . controllers ,
1081+ pilots_online : this . connectionCounts . pilots ,
1082+ observers_online : this . connectionCounts . observers ,
1083+ } ) ;
10371084 }
10381085
1039- private async trackDisconnection ( ) {
1086+ private async trackDisconnection ( info : { controllerId : string ; type : ClientType ; airport : string } , reason ?: string ) {
10401087 await this . updateActiveConnections ( - 1 ) ;
10411088
10421089 // If no more connections, remove from active_objects
@@ -1049,6 +1096,17 @@ export class Connection {
10491096 console . warn ( 'Failed to delete active_objects on disconnect (non-fatal):' , e instanceof Error ? e . message : e ) ;
10501097 }
10511098 }
1099+
1100+ this . emitAnalytics ( 'ws_connection_closed' , {
1101+ airport : info . airport ,
1102+ clientType : info . type ,
1103+ reason : reason || 'unspecified' ,
1104+ socket_count : this . sockets . size ,
1105+ object_id : this . objectId ,
1106+ controllers_online : this . connectionCounts . controllers ,
1107+ pilots_online : this . connectionCounts . pilots ,
1108+ observers_online : this . connectionCounts . observers ,
1109+ } ) ;
10521110 }
10531111 private getObjectName ( ) : string {
10541112 // Create a descriptive name with format: airport/controllerCount/pilotCount/observerCount
@@ -1079,9 +1137,20 @@ export class Connection {
10791137 }
10801138 }
10811139
1082- private async trackMessage ( _clientType : ClientType ) {
1083- void _clientType ;
1084- // TODO add posthog tracking of message types
1140+ private trackMessage ( details : {
1141+ clientType : ClientType ;
1142+ messageType : Packet [ 'type' ] ;
1143+ airport : string ;
1144+ meta ?: Record < string , unknown > ;
1145+ } ) {
1146+ const props : Record < string , unknown > = {
1147+ airport : details . airport ,
1148+ clientType : details . clientType ,
1149+ messageType : details . messageType ,
1150+ socket_count : this . sockets . size ,
1151+ ...details . meta ,
1152+ } ;
1153+ this . emitAnalytics ( 'ws_message' , props ) ;
10851154 }
10861155
10871156 private async updateActiveConnections ( change : number ) {
@@ -1233,12 +1302,15 @@ export class Connection {
12331302 }
12341303
12351304 const patch = packet . data . sharedStatePatch as Record < string , unknown > ;
1305+ const patchKeyCount = Object . keys ( patch ) . length ;
1306+ let patchSize = 0 ;
12361307
12371308 // Validate patch structure and size
12381309 try {
12391310 const patchString = JSON . stringify ( patch ) ;
1311+ patchSize = patchString . length ;
12401312 const MAX_PATCH_SIZE = 10240 ; // 10KB limit
1241- if ( patchString . length > MAX_PATCH_SIZE ) {
1313+ if ( patchSize > MAX_PATCH_SIZE ) {
12421314 throw new Error ( `Patch size exceeds maximum allowed size of ${ MAX_PATCH_SIZE } characters` ) ;
12431315 }
12441316 } catch {
@@ -1260,6 +1332,16 @@ export class Connection {
12601332 // Broadcast to all clients (including sender)
12611333 this . broadcastSharedState ( airport , patch , controllerId ) ;
12621334
1335+ this . trackMessage ( {
1336+ clientType : 'controller' ,
1337+ messageType : 'SHARED_STATE_UPDATE' ,
1338+ airport,
1339+ meta : {
1340+ patchKeys : patchKeyCount ,
1341+ patchSize,
1342+ } ,
1343+ } ) ;
1344+
12631345 return updatedState ;
12641346 } catch ( error ) {
12651347 console . error ( `Shared state update error for controller ${ controllerId } :` , error ) ;
0 commit comments