Repository navigation
Test Centre: attach Oura ring diagnostics to the report bundle, and guard the review sheet against large streams - #570
Conversation
The Test Centre report bundle gathered report.txt + raw-capture.jsonl (the
WHOOP frame capture) + meta.json, but never the Oura ring's Tier-B JSONL
sidecars (oura-raw / oura-ibihr / oura-activity), so a maintainer could only
get the redacted log lines, not the raw corpora needed to reframe/validate.
Attach them WHEN PRESENT (mirroring raw-capture.jsonl), not behind a test
domain: they cut across Sleep / HRV / Connection / Sources, and file presence
is the honest gate (no ring used -> no files -> nothing attached, so an
Experimental ring needs no separate brand check). The ring UUID inside each
line is masked to <device> by the existing redactEntries pass; entry names are
normalized (id dropped) since redaction never touches names.
- ouraDiagnosticEntries()/normalizedOuraEntryName(): enumerate
<App Support>/OpenWhoop/Diagnostics for oura-{raw,ibihr,activity}-*.jsonl,
map to id-free names, keep the largest file per name (read-only; never
creates the dir so a bundle build has no side effects).
- capEntries(): generalize the raw-capture-only tail-trim to a trimmableNames
set and split the budget across trimmable files in proportion to size. With a
single trimmable file this is byte-identical to the prior behaviour (its share
is the whole remainder), so the existing cap test still passes.
- whoop.sqlite is deliberately NOT attached (large, binary, unredactable;
meta.json already reports db_bytes + row counts).
App-target Swift (CI doesn't cover it): built Strand (macOS) + NOOPiOS, and
TestBundleAssemblerTests green 8/8 (5 existing + 3 new).
Verified on test/oura-persist-activity-combined; to be retrofitted onto the
per-feature diagnostics branches (oura-sleep-stage-timeline-persist, the
activity/ibihr branches).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bj38eZ4JMejA813XhYvMk6
…t (fixes hang)
The review-before-share sheet renders every text bundle entry as a single
SwiftUI Text. When a large research stream is attached — the ~12 MB Oura
oura-raw.jsonl sidecar, or the WHOOP raw-capture — SwiftUI/CoreText tries to lay
out megabytes of text as one Text, allocating >1 GB of glyph storage and pinning
the main thread indefinitely, so generating a report bundle appears to hang.
Diagnosed from a main-thread sample: 100% of samples in
SheetContentRoot.sizeThatFits -> StyledTextLayoutEngine.sizeThatFits ->
CTLineCreateWithAttributedString -> TGlyphEncoder::EncodeChars, at a 1.1 GB
footprint.
ReportReviewGate.previewText now excludes the large raw streams by name
(screenshot.png + raw-capture.jsonl + the oura-{raw,ibihr,activity}.jsonl
sidecars) AND anything over a 1 MiB per-entry guard, so any oversized stream
(a future one, or a pathologically large report.txt) is named in the "attached"
note rather than laid out. report.txt / meta.json / last-crash.txt still show in
full, so the review still shows exactly the report body the user shares; the
excluded streams stay in the .zip and are named so the review stays honest.
Self-contained (no dependency on the Test Centre assembler), so it applies to
main and every feature branch that attaches diagnostics. App-target Swift (CI
doesn't cover it): built Strand (macOS), ReportReviewGateTests 4/4.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bj38eZ4JMejA813XhYvMk6
|
Reviewed both commits. This is careful work — the two-commit split is right (the guard exists to make the attach safe), the scope is honestly stated, and the tests are real rather than decorative. The proportional cap-split in particular is a clean generalization: reserve non-trimmable whole, share the remainder by size, floor-round so the sum can't breach the cap, keep each tail. Caveat on my verification: this is app-target Swift ( The one substantive thing: the redaction guarantee is conditional and untestedThe commit message states "the ring UUID inside each line is masked to
Not blocking — the attach path is dormant until the producers land, so nothing leaks today. But this PR is where the "redaction covers it" claim is made, so I'd either (a) add a redaction test now with a line shaped the way the producer will actually write it, or (b) make it a landing gate on the producer PRs: the ring id MUST be emitted as Minor (neither blocking)
One reframingThe hang isn't reproducible on Solid PR. The redaction contract is the only thing I'd want addressed before this class of file starts carrying real ring ids. |
… canonical uuidString) Addresses the PR review (ryanbr): redactEntries masks a ring id to <device> only because the producer writes it as a canonical dashed uuidString — the shape the dash-anchored UUID rule in LiveState.redactPii matches. A dashless or truncated id would leak verbatim into a bundle the user shares, silently. Broadening the regex to dashless hex is NOT the fix: the raw sidecar's `hex` field is a long raw-byte run that a greedier rule would shred. So the contract is pinned with a test instead of a wider scrub. testRedactionMasksOuraRingIdButKeepsRawHexIntact feeds a representative line in the exact OuraRawDumpLine.encode shape (schema/deviceId/utc/iso/hex) through redactEntries and asserts the id is masked to <device> AND the raw hex bytes survive byte-for-byte. A future producer PR that emits a non-canonical id fails here rather than shipping the id. Verified: xcodebuild StrandTests — TestBundleAssemblerTests + ReportReviewGateTests 13/13 pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016DN6cU2ERWZUS9tWYCjPvi
|
@ryanbr Thanks for the careful review — the redaction point is a good catch. Addressed in 9a16e7d. Redaction contract (the blocking concern). You're right that the mask holds only for a canonical dashed uuidString — I confirmed the dash-anchored rule in LiveState.redactPii masks canonical (and lowercased) ids but leaks a dashless or truncated one. Today's producer writes the id in canonical form, so it's masked, but nothing pinned that. I went with your first option — a redaction test rather than a wider regex — because broadening the rule to dashless hex would actively corrupt these files: the raw sidecar's hex field is a long raw-byte run, and a greedier hex rule would shred exactly the capture the diagnostic exists to preserve. So the safer fix is to hold the producer to canonical uuidString and lock it with a test. testRedactionMasksOuraRingIdButKeepsRawHexIntact feeds a representative line in the exact OuraRawDumpLine.encode shape (schema/deviceId/utc/iso/hex) through redactEntries and asserts both halves: the id is masked to , and the raw hex bytes survive byte-for-byte. A future producer PR that emits a non-canonical id fails here instead of silently shipping the id in a bundle a user shares — the landing gate, made executable. Verified: xcodebuild StrandTests → TestBundleAssemblerTests + ReportReviewGateTests, 13/13 pass. On the two minor notes — both accurate, leaving as-is unless you'd prefer otherwise:
|
…arity) (#624) Port the two defense-in-depth pieces of the iOS #570 hang fix that Android's ReportReviewGate was missing: 1. A ~1 MiB per-entry size guard. Android excluded the large streams by NAME only (raw-capture + the screenshot), so a pathologically large report.txt — or any future attached stream — would still be inlined into one Compose Text, the same class of layout choke #570 hardened iOS against. Now ANY entry over MAX_INLINE_BYTES is excluded regardless of name. 2. Name EVERY excluded stream in the "attached (not shown above)" note. Android previously listed only binaries, so raw-capture was excluded from the inline preview but never named — the review was slightly less honest than iOS. Now raw-capture + any oversized entry are named, so the user sees the whole bundle. Mirrors Strand/System/ReportReviewGate.swift (notShownInline + maxInlineBytes). Android-only; no iOS change. Three tests: raw-capture excluded-but-named, an oversized entry caught by the guard and named, and normal small files still shown in full with no note. compileFullDebugKotlin + ReportReviewGateTest (6/6) + i18n audit pass locally.
… follow-up) (#621) Follow-up to #570. That PR closed the ring-id leak with a *contract* — the whole-bundle UUID scrub (LiveState.redactPii) masks the id only when the producer writes it as a canonical dashed uuidString; a dashless or truncated id would leak verbatim, and a hardcoded test could only document that, not enforce it. #570 correctly rejected broadening the UUID regex to dashless hex, because a greedier rule would shred the raw `hex` capture field. Close the leak by construction instead: in redactEntries, for the Oura sidecars only, also mask the `deviceId` JSON VALUE by field name ("deviceId"\s*:\s*")[^"]*(") -> <device>. This is: - format-independent — any id shape (dashless, truncated) is masked, so the producer no longer has to get the id format exactly right; - safe for the capture — key-anchored to "deviceId", so it never touches `hex` (the whole reason #570 didn't broaden the scrub); - scoped to the sidecars, so a non-PII logical deviceId ("my-whoop") elsewhere stays readable. Runs after redactPii (no-op on an already-canonical id it masked; catches the non-canonical ones it can't). Two tests: a dashless id is masked while `hex` survives byte-for-byte (the direction #570's contract test couldn't enforce), and a non-sidecar deviceId is left readable. Apple-only (Android has no SwiftUI review sheet / doesn't attach these sidecars). App-target Swift, so not CI-covered — NOT compile-verified here (no local Xcode; GitHub Actions incident active). Logic reviewed; needs an xcodebuild StrandTests pass once Actions/Xcode is available.
…uard the review sheet against large streams (ryanbr#570) * Test Centre: attach Oura ring diagnostics JSONL to the report bundle The Test Centre report bundle gathered report.txt + raw-capture.jsonl (the WHOOP frame capture) + meta.json, but never the Oura ring's Tier-B JSONL sidecars (oura-raw / oura-ibihr / oura-activity), so a maintainer could only get the redacted log lines, not the raw corpora needed to reframe/validate. Attach them WHEN PRESENT (mirroring raw-capture.jsonl), not behind a test domain: they cut across Sleep / HRV / Connection / Sources, and file presence is the honest gate (no ring used -> no files -> nothing attached, so an Experimental ring needs no separate brand check). The ring UUID inside each line is masked to <device> by the existing redactEntries pass; entry names are normalized (id dropped) since redaction never touches names. - ouraDiagnosticEntries()/normalizedOuraEntryName(): enumerate <App Support>/OpenWhoop/Diagnostics for oura-{raw,ibihr,activity}-*.jsonl, map to id-free names, keep the largest file per name (read-only; never creates the dir so a bundle build has no side effects). - capEntries(): generalize the raw-capture-only tail-trim to a trimmableNames set and split the budget across trimmable files in proportion to size. With a single trimmable file this is byte-identical to the prior behaviour (its share is the whole remainder), so the existing cap test still passes. - whoop.sqlite is deliberately NOT attached (large, binary, unredactable; meta.json already reports db_bytes + row counts). App-target Swift (CI doesn't cover it): built Strand (macOS) + NOOPiOS, and TestBundleAssemblerTests green 8/8 (5 existing + 3 new). Verified on test/oura-persist-activity-combined; to be retrofitted onto the per-feature diagnostics branches (oura-sleep-stage-timeline-persist, the activity/ibihr branches). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Bj38eZ4JMejA813XhYvMk6 * Test Centre: don't inline large raw streams in the report review sheet (fixes hang) The review-before-share sheet renders every text bundle entry as a single SwiftUI Text. When a large research stream is attached — the ~12 MB Oura oura-raw.jsonl sidecar, or the WHOOP raw-capture — SwiftUI/CoreText tries to lay out megabytes of text as one Text, allocating >1 GB of glyph storage and pinning the main thread indefinitely, so generating a report bundle appears to hang. Diagnosed from a main-thread sample: 100% of samples in SheetContentRoot.sizeThatFits -> StyledTextLayoutEngine.sizeThatFits -> CTLineCreateWithAttributedString -> TGlyphEncoder::EncodeChars, at a 1.1 GB footprint. ReportReviewGate.previewText now excludes the large raw streams by name (screenshot.png + raw-capture.jsonl + the oura-{raw,ibihr,activity}.jsonl sidecars) AND anything over a 1 MiB per-entry guard, so any oversized stream (a future one, or a pathologically large report.txt) is named in the "attached" note rather than laid out. report.txt / meta.json / last-crash.txt still show in full, so the review still shows exactly the report body the user shares; the excluded streams stay in the .zip and are named so the review stays honest. Self-contained (no dependency on the Test Centre assembler), so it applies to main and every feature branch that attaches diagnostics. App-target Swift (CI doesn't cover it): built Strand (macOS), ReportReviewGateTests 4/4. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Bj38eZ4JMejA813XhYvMk6 * Test Centre: pin the Oura sidecar redaction contract (ring id must be canonical uuidString) Addresses the PR review (ryanbr): redactEntries masks a ring id to <device> only because the producer writes it as a canonical dashed uuidString — the shape the dash-anchored UUID rule in LiveState.redactPii matches. A dashless or truncated id would leak verbatim into a bundle the user shares, silently. Broadening the regex to dashless hex is NOT the fix: the raw sidecar's `hex` field is a long raw-byte run that a greedier rule would shred. So the contract is pinned with a test instead of a wider scrub. testRedactionMasksOuraRingIdButKeepsRawHexIntact feeds a representative line in the exact OuraRawDumpLine.encode shape (schema/deviceId/utc/iso/hex) through redactEntries and asserts the id is masked to <device> AND the raw hex bytes survive byte-for-byte. A future producer PR that emits a non-canonical id fails here rather than shipping the id. Verified: xcodebuild StrandTests — TestBundleAssemblerTests + ReportReviewGateTests 13/13 pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016DN6cU2ERWZUS9tWYCjPvi --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…#570 parity) (ryanbr#624) Port the two defense-in-depth pieces of the iOS ryanbr#570 hang fix that Android's ReportReviewGate was missing: 1. A ~1 MiB per-entry size guard. Android excluded the large streams by NAME only (raw-capture + the screenshot), so a pathologically large report.txt — or any future attached stream — would still be inlined into one Compose Text, the same class of layout choke ryanbr#570 hardened iOS against. Now ANY entry over MAX_INLINE_BYTES is excluded regardless of name. 2. Name EVERY excluded stream in the "attached (not shown above)" note. Android previously listed only binaries, so raw-capture was excluded from the inline preview but never named — the review was slightly less honest than iOS. Now raw-capture + any oversized entry are named, so the user sees the whole bundle. Mirrors Strand/System/ReportReviewGate.swift (notShownInline + maxInlineBytes). Android-only; no iOS change. Three tests: raw-capture excluded-but-named, an oversized entry caught by the guard and named, and normal small files still shown in full with no note. compileFullDebugKotlin + ReportReviewGateTest (6/6) + i18n audit pass locally.
…anbr#572 follow-up) (ryanbr#621) Follow-up to ryanbr#570. That PR closed the ring-id leak with a *contract* — the whole-bundle UUID scrub (LiveState.redactPii) masks the id only when the producer writes it as a canonical dashed uuidString; a dashless or truncated id would leak verbatim, and a hardcoded test could only document that, not enforce it. ryanbr#570 correctly rejected broadening the UUID regex to dashless hex, because a greedier rule would shred the raw `hex` capture field. Close the leak by construction instead: in redactEntries, for the Oura sidecars only, also mask the `deviceId` JSON VALUE by field name ("deviceId"\s*:\s*")[^"]*(") -> <device>. This is: - format-independent — any id shape (dashless, truncated) is masked, so the producer no longer has to get the id format exactly right; - safe for the capture — key-anchored to "deviceId", so it never touches `hex` (the whole reason ryanbr#570 didn't broaden the scrub); - scoped to the sidecars, so a non-PII logical deviceId ("my-whoop") elsewhere stays readable. Runs after redactPii (no-op on an already-canonical id it masked; catches the non-canonical ones it can't). Two tests: a dashless id is masked while `hex` survives byte-for-byte (the direction ryanbr#570's contract test couldn't enforce), and a non-sidecar deviceId is left readable. Apple-only (Android has no SwiftUI review sheet / doesn't attach these sidecars). App-target Swift, so not CI-covered — NOT compile-verified here (no local Xcode; GitHub Actions incident active). Logic reviewed; needs an xcodebuild StrandTests pass once Actions/Xcode is available.
…uard the review sheet against large streams (ryanbr#570) * Test Centre: attach Oura ring diagnostics JSONL to the report bundle The Test Centre report bundle gathered report.txt + raw-capture.jsonl (the WHOOP frame capture) + meta.json, but never the Oura ring's Tier-B JSONL sidecars (oura-raw / oura-ibihr / oura-activity), so a maintainer could only get the redacted log lines, not the raw corpora needed to reframe/validate. Attach them WHEN PRESENT (mirroring raw-capture.jsonl), not behind a test domain: they cut across Sleep / HRV / Connection / Sources, and file presence is the honest gate (no ring used -> no files -> nothing attached, so an Experimental ring needs no separate brand check). The ring UUID inside each line is masked to <device> by the existing redactEntries pass; entry names are normalized (id dropped) since redaction never touches names. - ouraDiagnosticEntries()/normalizedOuraEntryName(): enumerate <App Support>/OpenWhoop/Diagnostics for oura-{raw,ibihr,activity}-*.jsonl, map to id-free names, keep the largest file per name (read-only; never creates the dir so a bundle build has no side effects). - capEntries(): generalize the raw-capture-only tail-trim to a trimmableNames set and split the budget across trimmable files in proportion to size. With a single trimmable file this is byte-identical to the prior behaviour (its share is the whole remainder), so the existing cap test still passes. - whoop.sqlite is deliberately NOT attached (large, binary, unredactable; meta.json already reports db_bytes + row counts). App-target Swift (CI doesn't cover it): built Strand (macOS) + NOOPiOS, and TestBundleAssemblerTests green 8/8 (5 existing + 3 new). Verified on test/oura-persist-activity-combined; to be retrofitted onto the per-feature diagnostics branches (oura-sleep-stage-timeline-persist, the activity/ibihr branches). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Bj38eZ4JMejA813XhYvMk6 * Test Centre: don't inline large raw streams in the report review sheet (fixes hang) The review-before-share sheet renders every text bundle entry as a single SwiftUI Text. When a large research stream is attached — the ~12 MB Oura oura-raw.jsonl sidecar, or the WHOOP raw-capture — SwiftUI/CoreText tries to lay out megabytes of text as one Text, allocating >1 GB of glyph storage and pinning the main thread indefinitely, so generating a report bundle appears to hang. Diagnosed from a main-thread sample: 100% of samples in SheetContentRoot.sizeThatFits -> StyledTextLayoutEngine.sizeThatFits -> CTLineCreateWithAttributedString -> TGlyphEncoder::EncodeChars, at a 1.1 GB footprint. ReportReviewGate.previewText now excludes the large raw streams by name (screenshot.png + raw-capture.jsonl + the oura-{raw,ibihr,activity}.jsonl sidecars) AND anything over a 1 MiB per-entry guard, so any oversized stream (a future one, or a pathologically large report.txt) is named in the "attached" note rather than laid out. report.txt / meta.json / last-crash.txt still show in full, so the review still shows exactly the report body the user shares; the excluded streams stay in the .zip and are named so the review stays honest. Self-contained (no dependency on the Test Centre assembler), so it applies to main and every feature branch that attaches diagnostics. App-target Swift (CI doesn't cover it): built Strand (macOS), ReportReviewGateTests 4/4. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Bj38eZ4JMejA813XhYvMk6 * Test Centre: pin the Oura sidecar redaction contract (ring id must be canonical uuidString) Addresses the PR review (ryanbr): redactEntries masks a ring id to <device> only because the producer writes it as a canonical dashed uuidString — the shape the dash-anchored UUID rule in LiveState.redactPii matches. A dashless or truncated id would leak verbatim into a bundle the user shares, silently. Broadening the regex to dashless hex is NOT the fix: the raw sidecar's `hex` field is a long raw-byte run that a greedier rule would shred. So the contract is pinned with a test instead of a wider scrub. testRedactionMasksOuraRingIdButKeepsRawHexIntact feeds a representative line in the exact OuraRawDumpLine.encode shape (schema/deviceId/utc/iso/hex) through redactEntries and asserts the id is masked to <device> AND the raw hex bytes survive byte-for-byte. A future producer PR that emits a non-canonical id fails here rather than shipping the id. Verified: xcodebuild StrandTests — TestBundleAssemblerTests + ReportReviewGateTests 13/13 pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016DN6cU2ERWZUS9tWYCjPvi --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…#570 parity) (ryanbr#624) Port the two defense-in-depth pieces of the iOS ryanbr#570 hang fix that Android's ReportReviewGate was missing: 1. A ~1 MiB per-entry size guard. Android excluded the large streams by NAME only (raw-capture + the screenshot), so a pathologically large report.txt — or any future attached stream — would still be inlined into one Compose Text, the same class of layout choke ryanbr#570 hardened iOS against. Now ANY entry over MAX_INLINE_BYTES is excluded regardless of name. 2. Name EVERY excluded stream in the "attached (not shown above)" note. Android previously listed only binaries, so raw-capture was excluded from the inline preview but never named — the review was slightly less honest than iOS. Now raw-capture + any oversized entry are named, so the user sees the whole bundle. Mirrors Strand/System/ReportReviewGate.swift (notShownInline + maxInlineBytes). Android-only; no iOS change. Three tests: raw-capture excluded-but-named, an oversized entry caught by the guard and named, and normal small files still shown in full with no note. compileFullDebugKotlin + ReportReviewGateTest (6/6) + i18n audit pass locally.
…anbr#572 follow-up) (ryanbr#621) Follow-up to ryanbr#570. That PR closed the ring-id leak with a *contract* — the whole-bundle UUID scrub (LiveState.redactPii) masks the id only when the producer writes it as a canonical dashed uuidString; a dashless or truncated id would leak verbatim, and a hardcoded test could only document that, not enforce it. ryanbr#570 correctly rejected broadening the UUID regex to dashless hex, because a greedier rule would shred the raw `hex` capture field. Close the leak by construction instead: in redactEntries, for the Oura sidecars only, also mask the `deviceId` JSON VALUE by field name ("deviceId"\s*:\s*")[^"]*(") -> <device>. This is: - format-independent — any id shape (dashless, truncated) is masked, so the producer no longer has to get the id format exactly right; - safe for the capture — key-anchored to "deviceId", so it never touches `hex` (the whole reason ryanbr#570 didn't broaden the scrub); - scoped to the sidecars, so a non-PII logical deviceId ("my-whoop") elsewhere stays readable. Runs after redactPii (no-op on an already-canonical id it masked; catches the non-canonical ones it can't). Two tests: a dashless id is masked while `hex` survives byte-for-byte (the direction ryanbr#570's contract test couldn't enforce), and a non-sidecar deviceId is left readable. Apple-only (Android has no SwiftUI review sheet / doesn't attach these sidecars). App-target Swift, so not CI-covered — NOT compile-verified here (no local Xcode; GitHub Actions incident active). Logic reviewed; needs an xcodebuild StrandTests pass once Actions/Xcode is available.
Summary
Two related, deliberately-bundled changes to the Test Centre report bundle. They
ship together because the second exists to make the first safe — separating them
would merge a feature that can hang the app, then fix it. Each is its own commit.
Attach Oura ring diagnostics to the report bundle — when the ring's
Tier-B JSONL sidecars (raw notifications / IBI-HR / activity MET) are present
in
<App Support>/OpenWhoop/Diagnostics, they're added to the bundle(mirroring
raw-capture.jsonl), redacted by the existing whole-bundle scrub(the ring UUID →
<device>), and trimmed to the 20 MB cap alongsideraw-capture. Attached when present — no separate brand gate: a run with no
ring produces no files, so nothing is attached. Entry names are normalized to
drop the ring id.
Don't inline large raw streams in the review sheet (fixes a hang) — the
review-before-share sheet rendered every text entry as a single SwiftUI
Text. A ~12 MBoura-raw.jsonlmade SwiftUI/CoreText try to lay outmegabytes as one
Text, allocating >1 GB of glyph storage and pinning themain thread indefinitely, so generating a report appeared to hang.
ReportReviewGate.previewTextnow excludes the large raw streams by name(screenshot + raw-capture + the Oura sidecars) plus a 1 MiB per-entry size
guard, naming them in the "attached" note instead.
report.txt/meta.json/
last-crash.txtstill show in full, and the excluded streams remain in the.zip— the review still shows exactly what's shared.How the hang was found
Main-thread
sample: 100% of samples in one synchronous layout pass at a 1.1 GBfootprint —
SheetContentRoot.sizeThatFits → StyledTextLayoutEngine.sizeThatFits → CTLineCreateWithAttributedString → TGlyphEncoder::EncodeChars.Read, redaction, and zip were all ruled out; this is purely UI text layout.
Scope
(
OuraRawDump/OuraIbiHrDump/OuraActivityDump) are not onmain, so theattach logic is dormant until they land — it safely attaches "when present"
and no-ops otherwise. The review-sheet guard is self-contained and correct
regardless.
Apple Test Centre; Android's report bundle does not attach these sidecars and
has no SwiftUI review sheet, so there is no Kotlin twin to change here.
Verification
App-target Swift (CI doesn't cover it): built Strand (macOS) on
origin/main;TestBundleAssemblerTests+ReportReviewGateTests12/12 pass, including anew test proving a 2 MB stream never reaches the preview. Hang fix confirmed
on-device — the report sheet now opens instantly.