Repository navigation
Conversation
Public ReportAttachmentInput ({name, data, mimeType?, fileName?}), writable via
ReportHandler.before (report.userAttachments / addReportAttachment) and
UploadOptions.attachments. Mobile limits (3 x 3 MB) enforced at assembly; drops go to
onError. Bundle: <random20>.attachment.bgsfile + manifest name/attrs (Android layout).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
There was a problem hiding this comment.
Deep review: T3 user attachments
Reviewed 5e350aa (feat(core): user attachments on reports) against the assembler, marker stores, Electron renderer IPC, and WebView report pipeline — not the diff alone. Live Node/browser upload() of in-memory attachments is well-tested. The holes are the JSON boundaries this payload now has to cross, and two paths that can still abort the whole report.
Findings
P1 — Binary attachments do not survive JSON, which is how crash recovery, Electron renderers, and WebView actually transport the report
- Location:
packages/core/src/attachments.ts:85(instanceof Uint8Array); persisted viauserAttachmentsonReport(reporting.ts:76) - Problem:
datamust be a string or a liveUint8Array.JSON.stringify(new Uint8Array([1,2,3]))becomes{"0":1,"1":2,"2":3}; NodeBufferbecomes{"type":"Buffer","data":[...]}. After parse,instanceof Uint8Arrayis false and the attachment is dropped as malformed. Reproduced. - Impact: The file comment says attachments usually ride a crash report that is the only record of the incident. That is exactly the path that JSON-persists the request before assembly.
- Scenario:
- Capture recovery (Node + browser):
submitReportwritesJSON.stringify(marker)(node-utilsreport-marker-store.ts:21, browser-utilsidb-report-marker-store.ts:17-18) before async assembly. If the process dies in that window — the window recovery exists for — next launch reassembles from the parsed marker and drops every binary attachment. Stringdatasurvives. A 64 KiBUint8Arrayalready JSON-encodes to ~644 KiB (~10×); 3 MB is tens of MB of"0":n,"1":n,...on the crash path. - Electron renderer:
encodeReportisJSON.stringify({source, report}). Main never sees aUint8Array. Binaryclient.upload({attachments})/addReportAttachmentfrom a renderer is dropped at assembly. Live path, not just recovery. - WebView:
webview-report-pipeline.tsJSON.stringify({ source, report })— same.
- Capture recovery (Node + browser):
- Fix: Give binary payloads a JSON-stable encoding (e.g.
{encoding:'base64', data}or a separate bytes blob keyed from the marker). Decode inresolveUserAttachmentsbefore theinstanceofcheck. Do not put rawUint8ArraythroughJSON.stringify. Copy bytes into the zip (or a sidecar) before the marker write if the crash path cannot wait.
P2 — Non-string fileName throws and loses the report
- Location:
packages/core/src/attachments.ts:123→sanitizeAttachmentFileName(:40,raw.replace) - Problem:
nameis type-checked;fileNameis not.0,false, or[]is not nullish, so it is passed through.sanitizeAttachmentFileNameassumes a string and throws. The throw escapesresolveUserAttachments/assembleBundle. The trigger pipeline maps it tobundle assembly failedand the incident is gone. Reproduced: a later valid attachment in the same array never runs;onErroris not called. - Impact: Directly contradicts the module contract (“a report is never lost”).
- Scenario:
{ name: 'log', data: dump, fileName: dump.byteLength }(wrong field) on a crashbeforehandler. Typed TS callers are safe; JS and inferred JSON are not. - Fix: If
fileNameis present and not a string, drop that attachment viaonError(same as a missingname). Never throw out ofresolveUserAttachments.
P2 — report.attachments is still the unsanitized zip-name path
- Location:
packages/core/src/reporting.ts:70vs newuserAttachments(:76); assemblerbundle-assembler.ts:207-210vs:216-226 - Problem: T3’s plan says
report.attachmentswritable frombefore. The safe path isuserAttachments/addReportAttachment.Report.attachmentsremains writable, skips the 3×3 MB cap, and usesattachment.nameas the zip member.writeBundleZipthrows on duplicate names /__proto__and aborts the entire bundle (already measured indocs/review/core-D-bundle-upload-recovery.md). - Impact: One wrong-field assignment on a crash handler can delete the incident (
name: 'manifest.json'/'logs.json'/'crash.json'). - Scenario:
beforedoesrequest.report.attachments = [{ name: 'logs.json', data }]because that is the Android-shaped name and the plan’s wording. - Fix: Keep
attachmentsinternal (readonly, or a non-exported type). Route application files only throughuserAttachments. If a caller assigns the old field, treat it as user input (namespace + limits) or reject it viaonErrorwithout throwing.
Residual (not blocking)
addReportAttachmentis exported from@bugsee/coreonly. Platform/umbrella barrels re-exportReportAttachmentInputbut not the helper, despite the “without reaching into core” pattern.- Recovery
assembleBundlecontext (node/browser/webworkerlaunch) still omitsonError, so recovered drops are silent even after the P1 is fixed. - Viewer use of
attrs.mimeType/attrs.fileNameremains unverified (known gap in the PR body).
Overall
- Overall risk: High
- Merge recommendation: Request changes — do not treat T3 as done until binary data survives markers and the renderer/WebView JSON hops, and until
resolveUserAttachmentscannot abort assembly. - Fix first: (1) JSON-stable binary encoding + decode before
instanceof, (2) never throw fromsanitizeAttachmentFileName/resolveUserAttachments, (3) stop exposing the unsanitizedreport.attachmentszip-name field as something application code can assign. - What is in good shape: Zip members are random
*.attachment.bgsfile(closes the zip-slip / reserved-name collisions for the new path).fileNametraversal is stripped. Oversize attachments do not consume a slot. Internal minidumps still coexist. Unit, assembler, client, and e2e coverage of the live in-memory path is real.
Sent by Cursor Automation: Bugsee code review
| if ( | ||
| typeof candidate !== 'object' || | ||
| candidate === null || | ||
| typeof candidate.name !== 'string' || | ||
| candidate.name === '' || | ||
| (typeof candidate.data !== 'string' && !(candidate.data instanceof Uint8Array)) |
There was a problem hiding this comment.
P1: This instanceof Uint8Array check is what drops binary attachments after every JSON hop.
JSON.stringify(new Uint8Array([1,2,3])) is {"0":1,"1":2,"2":3}; Node Buffer becomes {"type":"Buffer","data":[1,2,3]}. After JSON.parse, this condition fails and the attachment is reported malformed and dropped. String data round-trips. Reproduced against this function.
That hop is not hypothetical:
- Report markers persist
JSON.stringify(marker)before assembly (node-utilsreport-marker-store.ts:21,browser-utilsidb-report-marker-store.ts:17-18). A crash that dies in the drain/zip window — the window those markers exist for — recovers the report with every binary attachment gone. A 64 KiBUint8Arrayalready expands to ~644 KiB of index-keyed JSON on that crash path. - Electron renderer
encodeReportand WebViewJSON.stringify({ source, report })are JSON for the live path. Binaryupload({attachments})/addReportAttachmentfrom those environments never arrives as aUint8Array.
The comment at the top of this file says the attachment usually rides a crash report that is the only record of the incident. Encode bytes in a JSON-stable form (base64 tagged object, or a sidecar the marker points at) and decode here before this check. Do not JSON.stringify a raw Uint8Array.
| mimeType: | ||
| candidate.mimeType ?? | ||
| (typeof data === 'string' ? 'text/plain' : 'application/octet-stream'), | ||
| fileName: sanitizeAttachmentFileName(candidate.fileName ?? name), |
There was a problem hiding this comment.
P2: fileName is not type-checked, and sanitizeAttachmentFileName assumes a string (raw.replace at line 40). A non-string non-nullish value (0, false, []) skips ?? name and throws.
That throw leaves resolveUserAttachments / assembleBundle. The trigger pipeline turns it into bundle assembly failed and the incident is lost — including later valid attachments in the same array. onError is not called. Reproduced with { name: 'ok', data: 'x', fileName: 0 } followed by a good entry.
This is the hole in the “malformed → drop via onError, never throw” contract that name already honors. If fileName is present and not a string, drop this attachment through onError and continue.
| @@ -52,6 +68,12 @@ export interface Report { | |||
| crash?: CrashJson | NativeCrashJson; | |||
| /** Extra binary/text files written verbatim into the bundle (e.g. a native-crash `.dmp`). */ | |||
| attachments?: readonly ReportAttachment[]; | |||
There was a problem hiding this comment.
P2: This field is still writable application-facing state, and it is the unsafe attachment path. The assembler still uses attachment.name as the zip member (bundle-assembler.ts:207-210) with no 3×3 MB cap and no sanitization.
writeBundleZip throws on a duplicate name or __proto__ and aborts the entire bundle (already measured: manifest.json / logs.json / crash.json / apptoken → report lost). T3’s plan tells people to write report.attachments from before; the safe new field is userAttachments immediately below.
Keep this for SDK-owned minidumps (readonly / internal type). Application files should only go through userAttachments. Assigning here with name: 'logs.json' on a report that also captured logs deletes the incident.
…s survive the renderer/WebView JSON hops, report-level attachment names cannot lose a report, performanceUploadMode realtime is honoured - PR #38/#39 reviews 🤖 Generated with [Claude Code](https://claude.com/claude-code)
|
Review findings triaged against current main (this PR branch is a frozen snapshot, so its diff does not show the fixes): Binary user attachments now cross the Electron renderer and WebView JSON hops as base64 and SDK .dmp survives the recovery marker (dc5fc29, f1982c5); report.attachments names can no longer abort assembly; hostile attachment lists are contained. The overview PR #46 (head = main) was re-reviewed at ba608c4: no remaining findings, recommendation Approve. 🤖 Generated with Claude Code |
|
Closing instead of merging: this review-only branch is a frozen snapshot that now conflicts with the review base branch after #37/#40/#43/#46 were merged. Its content is already on 🤖 Generated with Claude Code |


Mobile parity: Android
Report.addAttachment, iOSBugseeAttachment.ReportAttachmentInput{name, data, mimeType?, fileName?}; writable fromReportHandler.before(addReportAttachment) andUploadOptions.attachments.onError. File names sanitised; zip member names are random (<random20>.attachment.bgsfile), manifest entry carriesname/mimeType/fileName.Review focus / known gaps
worker/jobs/bundle.py,manifest_entries.py), not against a running worker/viewer; how the viewer usesattrs.mimeType/fileNameis unchecked.File/byte[]variants (input isUint8Array | string).main:attachmentswas moved back onto the publicUploadOptionsafter merging with T2.🤖 Generated with Claude Code