diff --git a/src/browser/history/SazCapture.ts b/src/browser/history/SazCapture.ts index d36dc51d91..abb7f7caa2 100644 --- a/src/browser/history/SazCapture.ts +++ b/src/browser/history/SazCapture.ts @@ -13,6 +13,20 @@ import { decodeSazHttp } from "./SazHttp.js"; import { decodeSazMetadata } from "./SazMetadata.js"; import { projectSazRecord, type SazValue } from "./SazProjection.js"; +const SESSION_MEMBER_KINDS = { + "c.txt": "request", + "s.txt": "response", + "m.xml": "metadata", + "w.txt": "websocket", + "g.txt": "grpc", +} as const; +type SessionMemberKind = + (typeof SESSION_MEMBER_KINDS)[keyof typeof SESSION_MEMBER_KINDS]; +interface SessionMember { + readonly entry: ArtifactEntry; + readonly kind: SessionMemberKind; +} + /** Decode native SAZ sessions without extracting files, running Fiddler or converting to HAR. */ export const decodeSazCapture = async ( snapshotPath: string, @@ -24,7 +38,7 @@ export const decodeSazCapture = async ( WEB_NETWORK_CAPTURE_LIMITS.inputBytes, ); try { - const groups = new Map(); + const groups = new Map(); const inventory: SazValue[] = []; const paths = new Set(); let declaredBytes = 0; @@ -61,12 +75,19 @@ export const decodeSazCapture = async ( bytes: entry.declaredSize, }); if (entry.kind !== "file") continue; - const match = /^raw\/(\d+)_(c\.txt|s\.txt|m\.xml|w\.txt|g\.txt)$/.exec( - entry.path, - ); - if (match?.[1] !== undefined) { + const match = /^raw\/(\d+)_(.+)$/.exec(entry.path); + if ( + match?.[1] !== undefined && + match[2] !== undefined && + Object.hasOwn(SESSION_MEMBER_KINDS, match[2]) + ) { const group = groups.get(match[1]) ?? []; - group.push(entry); + group.push({ + entry, + kind: SESSION_MEMBER_KINDS[ + match[2] as keyof typeof SESSION_MEMBER_KINDS + ], + }); groups.set(match[1], group); } else if (entry.path.startsWith("raw/")) { throw new CaptureFormatError( @@ -91,7 +112,7 @@ export const decodeSazCapture = async ( grpc: null, }; const limitations: string[] = []; - for (const entry of entries) { + for (const { entry, kind } of entries) { const stream = await reader.open(entry); const chunks: Buffer[] = []; let length = 0; @@ -115,29 +136,24 @@ export const decodeSazCapture = async ( "SAZ member does not match its declared size.", ); const bytes = Buffer.concat(chunks, length); - const http = entry.path.endsWith("_c.txt") - ? "request" - : entry.path.endsWith("_s.txt") - ? "response" - : null; - if (http !== null) { + if (kind === "request" || kind === "response") { // Fiddler stores a session without this message as a zero-byte member. if (length === 0) limitations.push( - `The stored ${http} member is empty (zero bytes); the ${http} is reported as null.`, + `The stored ${kind} member is empty (zero bytes); the ${kind} is reported as null.`, ); else - source[http] = { + source[kind] = { member: entry.path, - ...decodeSazHttp(bytes, http === "request"), + ...decodeSazHttp(bytes, kind === "request"), }; - } else if (entry.path.endsWith("_m.xml")) + } else if (kind === "metadata") source.metadata = { member: entry.path, document: decodeSazMetadata(bytes), }; else - source[entry.path.endsWith("_w.txt") ? "websocket" : "grpc"] = { + source[kind] = { member: entry.path, messages: bytes, }; @@ -146,7 +162,7 @@ export const decodeSazCapture = async ( ordinal: records.length, location: { kind: "archive-members", - members: entries.map((entry) => entry.path), + members: entries.map(({ entry }) => entry.path), }, ...projectSazRecord(source, sensitiveValues), limitations,