From 4524bbd8dfec420d235b085caad5e17cc41fadb6 Mon Sep 17 00:00:00 2001 From: morluto <76467478+morluto@users.noreply.github.com> Date: Fri, 23 Oct 2026 18:43:41 +0800 Subject: [PATCH] fix(qlog): mark records with unknown STREAM FIN incomplete Keep known STREAM ranges and the unknown FIN value, while listing the source event among incomplete fact records so summary and retained views agree. Fixes #6598 --- docs/web-network-captures.md | 2 +- src/browser/history/QlogCapture.test.ts | 32 ++++++++++++++++++++++ src/browser/history/QlogCapture.ts | 2 ++ tests/acceptance/tools/qlogCapture.test.ts | 18 ++++++++---- 4 files changed, 47 insertions(+), 7 deletions(-) diff --git a/docs/web-network-captures.md b/docs/web-network-captures.md index 5a5f75ef42..4cd975ff99 100644 --- a/docs/web-network-captures.md +++ b/docs/web-network-captures.md @@ -820,7 +820,7 @@ by null; there is no single events pointer shared by all traces. | Original event and loss declaration | Observed endpoint report; `recovery:packet_lost` does not prove actual network loss. Missing packet number/type remains unknown. | | STREAM ranges and byte totals | Derived per trace and direction from reported offsets and lengths using exact integer arithmetic. `unique_bytes` unions intervals; `frame_length_sum` includes overlaps; `repeated_bytes` is their difference. Overlap alone does not prove retransmission or equal contents. | | FIN final sizes | Reported offset plus length where FIN is true; multiple conflicting sizes remain visible. A final size does not establish complete coverage or a received body. | -| Incomplete fact records | Ordinals with omitted/redacted interpretable frame metadata; usable fields in other records survive. An empty list does not establish complete logging or capture. | +| Incomplete fact records | Ordinals with omitted/redacted interpretable frame metadata, including an unknown STREAM FIN; usable fields such as known ranges in that record survive. An empty list does not establish complete logging or capture. | | HTTP bodies and causality | `body_bytes: "not-reported"`; lengths and headers are metadata, with no reconstructed body, hash, timing causality or live process attribution. | QUIC integer fields interpreted here accept exact nonnegative JSON integer diff --git a/src/browser/history/QlogCapture.test.ts b/src/browser/history/QlogCapture.test.ts index 2b2942b2e6..3b99c2e758 100644 --- a/src/browser/history/QlogCapture.test.ts +++ b/src/browser/history/QlogCapture.test.ts @@ -134,6 +134,38 @@ it("keeps incomplete fields unknown and FIN without bytes as metadata", () => { expect(traces[0]?.loss_declarations[0]?.packet_number.state).toBe("unknown"); }); +it.each([ + { fin: undefined, incomplete: [0] }, + { fin: null, incomplete: [0] }, + { fin: false, incomplete: [] }, + { fin: true, incomplete: [] }, +])( + "retains STREAM ranges and marks unknown FIN records incomplete: $fin", + ({ fin, incomplete }) => { + const streamFrame: Record = { + frame_type: "stream", + stream_id: 4, + offset: 8, + length: 3, + }; + if (fin !== undefined) streamFrame.fin = fin; + const { traces } = facts(capture([event([streamFrame])])); + expect(traces[0]?.incomplete_fact_records).toEqual(incomplete); + expect(traces[0]?.streams[0]?.contributions).toEqual([ + { + record_ordinal: 0, + frame_index: 0, + offset: "8", + length: "3", + fin: typeof fin === "boolean" ? fin : null, + }, + ]); + expect(traces[0]?.streams[0]?.covered_ranges).toEqual([ + { start: "8", end: "11" }, + ]); + }, +); + it("excludes structural auth material and explicit normalized values without guessing extension secrecy", () => { const text = capture([ { diff --git a/src/browser/history/QlogCapture.ts b/src/browser/history/QlogCapture.ts index 245ec1fd85..f740fbd52f 100644 --- a/src/browser/history/QlogCapture.ts +++ b/src/browser/history/QlogCapture.ts @@ -312,6 +312,8 @@ const collectTransportFacts = ( length: length.toString(), fin: typeof frame.fin === "boolean" ? frame.fin : null, }); + if (typeof frame.fin !== "boolean") + fact.incomplete_fact_records.push(ordinal); } }; diff --git a/tests/acceptance/tools/qlogCapture.test.ts b/tests/acceptance/tools/qlogCapture.test.ts index eafaf0ad4f..3691c685dc 100644 --- a/tests/acceptance/tools/qlogCapture.test.ts +++ b/tests/acceptance/tools/qlogCapture.test.ts @@ -29,9 +29,7 @@ const event = { time: 1791819895000.25, data: { header: { packet_type: "1RTT", packet_number: 5 }, - frames: [ - { frame_type: "stream", stream_id: 0, offset: 10, length: 5, fin: true }, - ], + frames: [{ frame_type: "stream", stream_id: 0, offset: 10, length: 5 }], }, }; const qlog = (events: unknown[]) => @@ -60,7 +58,7 @@ const assertAdditionalQlogFacets = async ( { kind: "qlog", facet: "loss-declarations", trace_index: 0 }, [{ record_ordinal: 1, packet_number: { state: "reported", value: "5" } }], ], - [{ kind: "qlog", facet: "incomplete-records", trace_index: 0 }, []], + [{ kind: "qlog", facet: "incomplete-records", trace_index: 0 }, [0]], [ { kind: "qlog", @@ -79,7 +77,7 @@ const assertAdditionalQlogFacets = async ( stream_id: "0", direction: "sent", }, - ["15"], + [], ], [{ kind: "qlog", facet: "traces", offset: 99 }, []], ] as const) { @@ -206,7 +204,15 @@ it("pages original trace-scoped facts after source deletion without conflating s ); expect(contribution.normalized_result).toMatchObject({ page: { - items: [{ record_ordinal: 0, frame_index: 0, offset: "10", length: "5" }], + items: [ + { + record_ordinal: 0, + frame_index: 0, + offset: "10", + length: "5", + fin: null, + }, + ], }, coverage: { total: 1, exhausted: true }, });