@@ -12,8 +12,9 @@ import {
12
12
HttpExchange ,
13
13
CollectedEvent ,
14
14
TimingEvents ,
15
- InputTlsFailure ,
16
- FailedTlsConnection
15
+ FailedTlsConnection ,
16
+ InputWebSocketMessage ,
17
+ InputTlsFailure
17
18
} from '../../types' ;
18
19
19
20
import { stringToBuffer } from '../../util' ;
@@ -62,9 +63,23 @@ export interface ExtendedHarRequest extends HarFormat.Request {
62
63
}
63
64
64
65
export interface HarEntry extends HarFormat . Entry {
66
+ _resourceType ?: 'websocket' ;
67
+ _webSocketMessages ?: HarWebSocketMessage [ ] ;
68
+ _webSocketClose ?: {
69
+ code ?: number ;
70
+ reason ?: string ;
71
+ timestamp ?: number ;
72
+ } | 'aborted'
65
73
_pinned ?: true ;
66
74
}
67
75
76
+ export interface HarWebSocketMessage {
77
+ type : 'send' | 'receive' ;
78
+ opcode : 1 | 2 ;
79
+ data : string ;
80
+ time : number ; // Epoch timestamp, as a float in seconds
81
+ }
82
+
68
83
export type HarTlsErrorEntry = {
69
84
startedDateTime : string ;
70
85
time : number ; // Floating-point high-resolution duration, in ms
@@ -354,7 +369,8 @@ async function generateHarHttpEntry(
354
369
? timingEvents . responseSentTimestamp ! - timingEvents . headersSentTimestamp !
355
370
: 0 ;
356
371
357
- const endTimestamp = timingEvents . responseSentTimestamp ??
372
+ const endTimestamp = timingEvents . wsClosedTimestamp ??
373
+ timingEvents . responseSentTimestamp ??
358
374
timingEvents . abortedTimestamp ;
359
375
360
376
const totalDuration = endTimestamp
@@ -387,7 +403,16 @@ async function generateHarHttpEntry(
387
403
_resourceType : 'websocket' ,
388
404
_webSocketMessages : exchange . messages . map ( ( message ) =>
389
405
generateHarWebSocketMessage ( message , timingEvents )
390
- )
406
+ ) ,
407
+ _webSocketClose : exchange . closeState && exchange . closeState !== 'aborted'
408
+ ? {
409
+ code : exchange . closeState . closeCode ,
410
+ reason : exchange . closeState . closeReason ,
411
+ timestamp : timingEvents . wsClosedTimestamp
412
+ ? timingEvents . wsClosedTimestamp / 1000 // Match _webSocketMessage format
413
+ : undefined
414
+ }
415
+ : exchange . closeState
391
416
} : { } )
392
417
} ;
393
418
}
@@ -461,8 +486,9 @@ export async function parseHar(harContents: unknown): Promise<ParsedHar> {
461
486
462
487
har . log . entries . forEach ( ( entry , i ) => {
463
488
const id = baseId + i ;
489
+ const isWebSocket = entry . _resourceType === 'websocket' ;
464
490
465
- const timingEvents : TimingEvents = Object . assign ( {
491
+ const timingEvents : TimingEvents = {
466
492
startTime : dateFns . parse ( entry . startedDateTime ) . getTime ( ) ,
467
493
startTimestamp : 0 ,
468
494
bodyReceivedTimestamp : sumTimings ( entry . timings ,
@@ -478,44 +504,92 @@ export async function parseHar(harContents: unknown): Promise<ParsedHar> {
478
504
'send' ,
479
505
'wait'
480
506
)
481
- } , entry . response . status !== 0
482
- ? { responseSentTimestamp : entry . time }
483
- : { abortedTimestamp : entry . time }
507
+ } ;
508
+
509
+ Object . assign ( timingEvents ,
510
+ entry . response . status !== 0
511
+ ? { responseSentTimestamp : entry . time }
512
+ : { abortedTimestamp : entry . time } ,
513
+
514
+ isWebSocket
515
+ ? {
516
+ wsAcceptedTimestamp : timingEvents . headersSentTimestamp ,
517
+ wsClosedTimestamp : entry . time
518
+ }
519
+ : { }
484
520
) ;
485
521
522
+
486
523
const request = parseHarRequest ( id , entry . request , timingEvents ) ;
487
- events . push ( { type : 'request' , event : request } ) ;
524
+
525
+ events . push ( {
526
+ type : isWebSocket ? 'websocket-request' : 'request' ,
527
+ event : request
528
+ } ) ;
488
529
489
530
if ( entry . response . status !== 0 ) {
490
531
events . push ( {
491
- type : 'response' ,
532
+ type : isWebSocket && entry . response . status === 101
533
+ ? 'websocket-accepted'
534
+ : 'response' ,
492
535
event : parseHarResponse ( id , entry . response , timingEvents )
493
536
} ) ;
494
537
} else {
495
538
events . push ( { type : 'abort' , event : request } ) ;
496
539
}
497
540
541
+ if ( isWebSocket ) {
542
+ events . push ( ...entry . _webSocketMessages ?. map ( message => ( {
543
+ type : `websocket-message-${ message . type === 'send' ? 'received' : 'sent' } ` as const ,
544
+ event : {
545
+ streamId : request . id ,
546
+ direction : message . type === 'send' ? 'received' : 'sent' ,
547
+ isBinary : message . opcode === 2 ,
548
+ content : Buffer . from ( message . data , message . opcode === 2 ? 'base64' : 'utf8' ) ,
549
+ eventTimestamp : ( message . time * 1000 ) - timingEvents . startTime ,
550
+ timingEvents : timingEvents ,
551
+ tags : [ ]
552
+ } satisfies InputWebSocketMessage
553
+ } ) ) ?? [ ] ) ;
554
+
555
+ const closeEvent = entry . _webSocketClose ;
556
+
557
+ if ( closeEvent && closeEvent !== 'aborted' ) {
558
+ events . push ( {
559
+ type : 'websocket-close' ,
560
+ event : {
561
+ streamId : request . id ,
562
+ closeCode : closeEvent . code ,
563
+ closeReason : closeEvent . reason ?? "" ,
564
+ timingEvents : timingEvents ,
565
+ tags : [ ]
566
+ }
567
+ } ) ;
568
+ } else {
569
+ // N.b. WebSockets can abort _after_ the response event!
570
+ events . push ( { type : 'abort' , event : request } ) ;
571
+ }
572
+ }
573
+
498
574
if ( entry . _pinned ) pinnedIds . push ( id ) ;
499
575
} ) ;
500
576
501
577
if ( har . log . _tlsErrors ) {
502
- har . log . _tlsErrors . forEach ( ( entry ) => {
503
- events . push ( {
504
- type : 'tls-client-error' ,
505
- event : {
506
- failureCause : entry . cause ,
507
- hostname : entry . hostname ,
508
- remoteIpAddress : entry . clientIPAddress ,
509
- remotePort : entry . clientPort ,
510
- tags : [ ] ,
511
- timingEvents : {
512
- startTime : dateFns . parse ( entry . startedDateTime ) . getTime ( ) ,
513
- connectTimestamp : 0 ,
514
- failureTimestamp : entry . time
515
- }
578
+ events . push ( ...har . log . _tlsErrors . map ( ( entry ) => ( {
579
+ type : 'tls-client-error' as const ,
580
+ event : {
581
+ failureCause : entry . cause ,
582
+ hostname : entry . hostname ,
583
+ remoteIpAddress : entry . clientIPAddress ,
584
+ remotePort : entry . clientPort ,
585
+ tags : [ ] ,
586
+ timingEvents : {
587
+ startTime : dateFns . parse ( entry . startedDateTime ) . getTime ( ) ,
588
+ connectTimestamp : 0 ,
589
+ failureTimestamp : entry . time
516
590
}
517
- } ) ;
518
- } ) ;
591
+ }
592
+ } ) ) ) ;
519
593
}
520
594
521
595
return { events, pinnedIds } ;
0 commit comments