Skip to content

Commit 9b314c2

Browse files
committed
Add support for importing websockets from a HAR
1 parent 58ab169 commit 9b314c2

File tree

4 files changed

+106
-27
lines changed

4 files changed

+106
-27
lines changed

src/model/events/events-store.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -636,7 +636,9 @@ export class EventsStore {
636636
// First, we run through the request & TLS error events together, in order, since these
637637
// define the initial event ordering
638638
const [initialEvents, updateEvents] = _.partition(events, ({ type }) =>
639-
type === 'request' || type === 'tls-client-error'
639+
type === 'request' ||
640+
type === 'websocket-request' ||
641+
type === 'tls-client-error'
640642
);
641643
this.eventQueue.push(..._.sortBy(initialEvents, (e) =>
642644
(e.event as { timingEvents: TimingEvents }).timingEvents.startTime

src/model/http/har.ts

Lines changed: 100 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,9 @@ import {
1212
HttpExchange,
1313
CollectedEvent,
1414
TimingEvents,
15-
InputTlsFailure,
16-
FailedTlsConnection
15+
FailedTlsConnection,
16+
InputWebSocketMessage,
17+
InputTlsFailure
1718
} from '../../types';
1819

1920
import { stringToBuffer } from '../../util';
@@ -62,9 +63,23 @@ export interface ExtendedHarRequest extends HarFormat.Request {
6263
}
6364

6465
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'
6573
_pinned?: true;
6674
}
6775

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+
6883
export type HarTlsErrorEntry = {
6984
startedDateTime: string;
7085
time: number; // Floating-point high-resolution duration, in ms
@@ -354,7 +369,8 @@ async function generateHarHttpEntry(
354369
? timingEvents.responseSentTimestamp! - timingEvents.headersSentTimestamp!
355370
: 0;
356371

357-
const endTimestamp = timingEvents.responseSentTimestamp ??
372+
const endTimestamp = timingEvents.wsClosedTimestamp ??
373+
timingEvents.responseSentTimestamp ??
358374
timingEvents.abortedTimestamp;
359375

360376
const totalDuration = endTimestamp
@@ -387,7 +403,16 @@ async function generateHarHttpEntry(
387403
_resourceType: 'websocket',
388404
_webSocketMessages: exchange.messages.map((message) =>
389405
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
391416
} : {})
392417
};
393418
}
@@ -461,8 +486,9 @@ export async function parseHar(harContents: unknown): Promise<ParsedHar> {
461486

462487
har.log.entries.forEach((entry, i) => {
463488
const id = baseId + i;
489+
const isWebSocket = entry._resourceType === 'websocket';
464490

465-
const timingEvents: TimingEvents = Object.assign({
491+
const timingEvents: TimingEvents = {
466492
startTime: dateFns.parse(entry.startedDateTime).getTime(),
467493
startTimestamp: 0,
468494
bodyReceivedTimestamp: sumTimings(entry.timings,
@@ -478,44 +504,92 @@ export async function parseHar(harContents: unknown): Promise<ParsedHar> {
478504
'send',
479505
'wait'
480506
)
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+
: {}
484520
);
485521

522+
486523
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+
});
488529

489530
if (entry.response.status !== 0) {
490531
events.push({
491-
type: 'response',
532+
type: isWebSocket && entry.response.status === 101
533+
? 'websocket-accepted'
534+
: 'response',
492535
event: parseHarResponse(id, entry.response, timingEvents)
493536
});
494537
} else {
495538
events.push({ type: 'abort', event: request });
496539
}
497540

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+
498574
if (entry._pinned) pinnedIds.push(id);
499575
});
500576

501577
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
516590
}
517-
});
518-
});
591+
}
592+
})));
519593
}
520594

521595
return { events, pinnedIds };

src/model/websockets/websocket-stream.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ export class WebSocketStream extends HttpExchange {
3939
if (_.isString(subprotocolHeader)) this.subprotocol = subprotocolHeader;
4040

4141
this.accepted = true;
42+
Object.assign(this.timingEvents, response.timingEvents);
4243
}
4344

4445
wasAccepted() {
@@ -63,6 +64,7 @@ export class WebSocketStream extends HttpExchange {
6364
@action
6465
markClosed(closeData: InputWebSocketClose) {
6566
this.closeData = closeData;
67+
Object.assign(this.timingEvents, closeData.timingEvents);
6668
}
6769

6870
get closeState() {

src/types.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ import type { RTCMediaTrack } from './model/webrtc/rtc-media-track';
4040
import type { TrafficSource } from './model/http/sources';
4141
import type { ViewableContentType } from './model/events/content-types';
4242

43+
// These are the HAR types as returned from parseHar(), not the raw types as defined in the HAR itself
4344
export type HarBody = { encodedLength: number, decoded: Buffer };
4445
export type HarRequest = Omit<MockttpCompletedRequest, 'body' | 'timingEvents' | 'matchedRuleId'> &
4546
{ body: HarBody; timingEvents: TimingEvents, matchedRuleId: false };

0 commit comments

Comments
 (0)