Repository navigation
feat: support adding and removing attachments after initialization - #26
Conversation
Crashpad freezes --attachment argv at StartHandlerAtCrash, so post-init updates never reached the handler. Add BugSplat.addAttachment/removeAttachment and a small PIE wrapper (libbugsplat_handler.so) that re-reads a list file at crash time before exec'ing crashpad_handler. Paths are resolved when a crash is uploaded, matching Windows and Apple, so a log file can be attached before it exists. Closes #25
There was a problem hiding this comment.
🟡 Changes recommended
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
Adds runtime attachment management to the BugSplat Android SDK by introducing addAttachment / removeAttachment APIs and implementing a Crashpad handler wrapper so attachment paths can be updated after initialization and resolved at upload time.
Changes:
- Add public Java APIs
BugSplat.addAttachment(path)/BugSplat.removeAttachment(path)plus bridge + JNI plumbing. - Persist attachment paths to
<crashpad_db>/bugsplat_attachments.listand introduce a PIE wrapper (libbugsplat_handler.so) that reads the list at crash time and execslibcrashpad_handler.sowith--attachment=args. - Add validation utility + unit tests, update README and example app to demonstrate post-init attachment usage, and update build packaging to ship the wrapper.
File summaries
| File | Description |
|---|---|
| README.md | Documents post-init attachment add/remove usage. |
| example/src/main/java/com/bugsplat/example/MainActivity.java | Demonstrates writing and attaching a log file after BugSplat.init. |
| app/src/test/java/com/bugsplat/android/AttachmentPathTest.java | Unit tests for attachment path validation (null/blank/newline rejection). |
| app/src/main/java/com/bugsplat/android/BugSplatBridge.java | Adds bridge-level add/remove attachment methods and new JNI declarations. |
| app/src/main/java/com/bugsplat/android/BugSplat.java | Exposes public SDK APIs with Javadoc for add/remove attachment. |
| app/src/main/java/com/bugsplat/android/AttachmentPath.java | Centralized path validation helper used by the new API surface. |
| app/src/main/cpp/native-lib.cpp | Implements attachment persistence, wrapper selection, and JNI add/remove attachment behavior. |
| app/src/main/cpp/include/bugsplat_utils.h | Updates createAttachments signature/contract to return strings and resolve at upload time. |
| app/src/main/cpp/include/bugsplat_attachments.h | Defines the shared list filename constant used by SDK + wrapper. |
| app/src/main/cpp/handler_wrapper.cpp | New exec-wrapper that reads the attachments list and appends --attachment= args. |
| app/src/main/cpp/CMakeLists.txt | Builds wrapper as PIE, copies into a Gradle-merged jniLibs directory, and links it. |
| app/build.gradle | Adds the wrapper output directory to jniLibs.srcDirs for packaging. |
Review details
- Files reviewed: 12/12 changed files
- Comments generated: 4
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Honor persist failures (roll back in-memory state; fall back to argv at init), validate init-time paths the same way as addAttachment, document that files are copied at crash-capture time, resolve crashpad_handler from argv[0] before /proc/self/exe, and fix JNI ReleaseStringUTFChars on a null GetStringUTFChars result.
bobbyg603
left a comment
There was a problem hiding this comment.
Summary
The wrapper-plus-list-file approach is the right way around Crashpad freezing --attachment argv at StartHandlerAtCrash, and the implementation is mostly solid: PIE libbugsplat_handler.so is packaged in the AAR for all three ABIs, the list is atomically replaced (tmp + fsync + rename), and post-init add/remove is mutex-protected. Crashpad still copies attachment bytes when the minidump is written (missing files skipped then), not at HTTP upload; the public docs get that wrong. Dominant remaining risks are silent attachment loss when list persistence fails (especially at init, where the wrapper path no longer puts paths in argv) and init-time paths bypassing the newline validation the new line-oriented file format requires.
Issue counts by severity
- bugs: 0
- suggestions: 4
- nits: 1
| /** | ||
| * Attach a file to subsequent native crash reports. | ||
| * This can be called at any time after {@link #init}, including with a path | ||
| * that does not exist yet — the file is read when a crash is uploaded. |
There was a problem hiding this comment.
[suggestion] The new API/Javadoc (and README / bugsplat_utils.h) say the file is read "when a crash is uploaded." Crashpad's Linux/Android handler copies --attachment files in WriteMinidumpToDatabase at crash-dump time (FileReader::Open failure → log and skip). A file that appears only after the process dies — for example on the next launch before pending reports upload — will not be attached. Native comments in native-lib.cpp already say "crash time"; the public contract should match that. "Does not need to exist yet" is still true at addAttachment time.
Suggestion: Say the path is resolved when the crash is captured (handler copies the file into the Crashpad database; missing files are skipped). Keep the "need not exist at add/init time" guarantee. Update README and bugsplat_utils.h the same way.
There was a problem hiding this comment.
Fixed in 29e6dca. Javadoc, README, and bugsplat_utils.h now say the path is copied when the crash is captured (missing files skipped). The file still need not exist at add/init time.
| pthread_mutex_lock(&g_attachments_mutex); | ||
| g_attachments = new vector<string>(createAttachments(env, attachments)); | ||
| g_attachments_list_path = reportsDir.value() + "/" + kAttachmentsListFileName; | ||
| persistAttachmentsLocked(); |
There was a problem hiding this comment.
[suggestion] persistAttachmentsLocked()'s return value is ignored at init, in addAttachmentPath, and in removeAttachmentPath. With the wrapper (the normal path), init-time attachments are no longer passed into StartHandlerAtCrash — they live only in bugsplat_attachments.list. A failed write therefore drops those attachments with no signal to the caller, and a failed add still mutates g_attachments, so a retry of the same path is a no-op while the wrapper keeps reading the old file.
Suggestion: Honor the persist result: on failure, roll back the in-memory change (or never commit until persist succeeds), and at init either log loudly or fall back to passing attachmentPaths into StartHandlerAtCrash so existing init attachments are not lost.
There was a problem hiding this comment.
Fixed in 29e6dca. Persist failures roll back in-memory state. At init, if persist fails we still pass the snapshot into StartHandlerAtCrash so those attachments are not dropped, and we log an error.
| // the wrapper, Crashpad snapshots the paths into argv here and they cannot | ||
| // be updated later. | ||
| pthread_mutex_lock(&g_attachments_mutex); | ||
| g_attachments = new vector<string>(createAttachments(env, attachments)); |
There was a problem hiding this comment.
[suggestion] AttachmentPath.validate rejects null/blank/newline paths for addAttachment/removeAttachment, but init(..., attachments) still feeds strings straight into createAttachments and then into the line-oriented list file. A path containing \n or \r becomes two --attachment arguments at crash time. That corruption did not exist when paths were FilePath objects in argv.
Suggestion: Validate (or skip and log) each init attachment with the same rules as AttachmentPath.validate before persisting.
There was a problem hiding this comment.
Fixed in 29e6dca. Init-time paths go through the same absolute / no-newline checks as addAttachment before they are written to the list file.
|
|
||
| static int realHandlerPath(char* out, size_t outSize) { | ||
| char self[PATH_MAX]; | ||
| ssize_t n = readlink("/proc/self/exe", self, sizeof(self) - 1); |
There was a problem hiding this comment.
[suggestion] The wrapper locates libcrashpad_handler.so via readlink("/proc/self/exe"). If that fails it returns 1 without exec'ing the real handler, so the crash is lost. Crashpad's StartHandlerAtCrash already passes the absolute wrapper path as argv[0]; that is a more direct sibling-path source than /proc/self/exe (which can fail or pick up a (deleted) suffix after an in-place APK update).
Suggestion: Derive the handler directory from argv[0] (fall back to /proc/self/exe if needed). If the sibling libcrashpad_handler.so still cannot be resolved, execv the original argv rather than exiting — a dump without extra attachments is better than none.
There was a problem hiding this comment.
Fixed in 29e6dca: the wrapper now resolves libcrashpad_handler.so from argv[0] first, then /proc/self/exe (stripping a (deleted) suffix).
Did not execv the original argv when lookup fails: argv[0] is this wrapper, so that would recurse. A dump still requires locating the sibling handler.
| @@ -0,0 +1,124 @@ | |||
| #include <android/log.h> | |||
| #include <dirent.h> | |||
There was a problem hiding this comment.
[nit] #include <dirent.h> is unused.
Suggestion: Remove it.
There was a problem hiding this comment.
Fixed in 29e6dca. Removed the unused #include <dirent.h>.
There was a problem hiding this comment.
🟡 Changes recommended
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
Suppressed comments (1)
Previously missed (1) — in code that hasn't changed since the last review.
app/src/main/cpp/native-lib.cpp:105
jniInitBugSplatcopies multipleGetStringUTFCharsresults intostd::stringbut never callsReleaseStringUTFChars, which leaks/pins the underlying Java string storage for the lifetime of the process (and also leaks on early returns). Consider copying intostd::stringand releasing immediately for each inputjstring.
string dataDir = env->GetStringUTFChars(data_dir, nullptr);
string libDir = env->GetStringUTFChars(lib_dir, nullptr);
// Crashpad file paths. Prefer the wrapper so post-init add/remove of
// attachments is reflected at crash time; fall back to the stock handler.
- Files reviewed: 12/12 changed files
- Comments generated: 1
- Review effort level: Lite
Callers had to know the reserved attribute names the backend promotes (BugSplatUser, BugSplatEmail, BugSplatNotes, BugSplatApplicationKey) and set them through setAttribute, which couples clients to a backend naming convention that nothing in this SDK documents. Native crashes reach BugSplat through Crashpad, which carries annotations and nothing else, so the four setters still write those attributes. ANRs are committed directly rather than through Crashpad, so the values are also retained and applied to the commit request as typed fields, which they previously never were. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013vehBwh7WFaVQiawtJa7AN
bugsplat-android accepted attachments only while it initialized, so AttachNativeLogFile was a documented no-op there and the paths this branch registers from PersistentDataFileAttachmentPaths reached every native platform except Android. BugSplat-Git/bugsplat-android#26 adds addAttachment and removeAttachment, so the existing registration path now works on Android with no change to how callers use it. Also switches the Android branches from BugSplatBridge to the public BugSplat class and adopts its new setUser, setEmail, setNotes, and setKey rather than promoting the reserved attribute names by hand. The bridge is the JNI surface, not the public API, and hiding it is only possible once nothing outside the AAR calls it. Requires a bugsplat-android build with #26 merged; the vendored AAR must be refreshed before these calls resolve on device. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013vehBwh7WFaVQiawtJa7AN
Nothing in the example exercised the four setters, so there was no way to confirm on the dashboard that they land in their own columns rather than as custom attributes. Set after init, alongside the attachment, so a crash, an ANR, or feedback from the session all carry them. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013vehBwh7WFaVQiawtJa7AN
There was a problem hiding this comment.
🔵 Needs a closer look
The new handler wrapper can mis-parse overlong list-file lines into unintended extra attachments; the parsing should be hardened before approval.
Review details
Suppressed comments (1)
Previously missed (1) — in code that hasn't changed since the last review.
app/src/main/cpp/handler_wrapper.cpp:113
- The wrapper reads attachment paths with
fgets(line, PATH_MAX, file)and then treats each buffer as a complete path. If a path line exceedsPATH_MAX-1(or the list file is corrupted),fgetsreturns a truncated chunk without a trailing newline; the nextfgetscall will read the remainder as a second “path”, producing unintended--attachment=args. Detect truncated reads and skip/drain the remainder of the overlong line before continuing.
- Files reviewed: 13/13 changed files
- Comments generated: 0 new
- Review effort level: Lite
There was a problem hiding this comment.
🔵 Needs a closer look
The new handler wrapper/selection logic needs robustness fixes (executability checks and safe parsing/truncation handling) to avoid initialization/crash-time failures and unintended attachments.
Review details
Suppressed comments (7)
Previously missed (4) — in code that hasn't changed since the last review.
app/src/main/cpp/handler_wrapper.cpp:61
handlerFromExePathonly checksF_OKforlibcrashpad_handler.so, but this wrapper immediatelyexecvs that path. If the file exists but is not executable,execvwill fail at crash time. CheckX_OKto ensure the resolved handler is actually executable.
app/src/main/cpp/handler_wrapper.cpp:123- The wrapper reads
bugsplat_attachments.listwith a fixedPATH_MAXbuffer and treats everyfgetschunk as a distinct attachment. If a line exceeds the buffer (or the file is corrupted), one path can be split into multiple unintended attachments (and the wrapper will also silently ignore any attachments beyondMAX_EXTRA_ATTACHMENTS). Detect and skip overlong lines, validate absolute paths, and log when the list is truncated.
app/src/main/cpp/native-lib.cpp:108 useWrapperonly checks thatlibbugsplat_handler.soexists. If the file is present but not executable (permissions/corrupt packaging), the SDK will still try to start it as the Crashpad handler and initialization can fail. Prefer checkingX_OK(or otherwise verifying executability) before selecting the wrapper.
app/src/main/java/com/bugsplat/android/BugSplat.java:130setUser's Javadoc implies it affects subsequent native crash reports, but if it’s called beforeinitthe native annotation list isn’t registered yet (native logs "setAttribute called before init" and drops the update). Either persist/apply the value on init or document that this should be called afterinitto affect native crashes.
This issue also appears in the following locations of the same file:
- line 140
- line 152
- line 164
app/src/main/java/com/bugsplat/android/BugSplat.java:142
- Same as
setUser: ifsetEmailis called beforeinit, the native attribute update is dropped because the annotation list isn’t initialized yet. Either apply stored values during init or document that callers must call this afterinitfor native crash reports.
* Set the user email reported with subsequent crash, ANR, and feedback reports.
*
* <p>Passing null clears the value.</p>
app/src/main/java/com/bugsplat/android/BugSplat.java:154
- Same as
setUser: ifsetNotesis called beforeinit, the native attribute update is dropped because the annotation list isn’t initialized yet. Either apply stored values during init or document that callers must call this afterinitfor native crash reports.
* Set the notes reported with subsequent crash, ANR, and feedback reports.
*
* <p>Passing null clears the value.</p>
app/src/main/java/com/bugsplat/android/BugSplat.java:166
- Same as
setUser: ifsetKeyis called beforeinit, the native attribute update is dropped because the annotation list isn’t initialized yet. Either apply stored values during init or document that callers must call this afterinitfor native crash reports.
* Set the application key reported with subsequent crash, ANR, and feedback reports.
*
* <p>Passing null clears the value.</p>
- Files reviewed: 13/13 changed files
- Comments generated: 0 new
- Review effort level: Lite
The success path sets newArgv[0] to the resolved libcrashpad_handler.so, but the allocation-failure fallback exec'd the original argv, whose argv[0] is this wrapper. The handler would then see itself named libbugsplat_handler.so on the one path where diagnosing what happened matters most. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013vehBwh7WFaVQiawtJa7AN
There was a problem hiding this comment.
🔵 Needs a closer look
There’s a confirmed persist-failure edge case that can cause stale attachments to be added at crash time, and the PR also introduces additional public API not described in the PR’s stated scope.
Review details
Suppressed comments (3)
Previously missed (3) — in code that hasn't changed since the last review.
app/src/main/cpp/handler_wrapper.cpp:113
- The handler wrapper currently turns any non-empty line from bugsplat_attachments.list into a --attachment= argument. Adding a minimal absolute-path check here hardens against a corrupted/stale list file producing malformed attachment args.
app/src/main/cpp/native-lib.cpp:164 - If persisting the attachments list fails and a stale bugsplat_attachments.list from a previous run exists, the wrapper will still read it at crash time and append outdated attachments on top of the argv snapshot. This can produce unexpected extra attachments even though init logged a persist failure. Consider deleting the list file on persist failure so the wrapper won’t pick up stale entries.
app/src/main/java/com/bugsplat/android/BugSplat.java:116 - This change adds new public API for promoted fields (BugSplatUser/BugSplatEmail/BugSplatNotes/BugSplatApplicationKey) along with new setters (setUser/setEmail/setNotes/setKey) and updates ANR commits to use them, but the PR description/linked issue describe only post-init attachment add/remove. Please either document these new APIs and call out the additional scope in the PR description, or move them to a separate PR to keep the attachment change focused.
- Files reviewed: 13/13 changed files
- Comments generated: 0 new
- Review effort level: Lite
Three review findings: Persisting the list at init could fail while a list file from a previous run remained on disk. The wrapper reads that file at crash time and appends it to the argv snapshot init falls back to, attaching stale paths this session never registered. Unlink the file when persisting fails. The wrapper turned any non-empty line into an --attachment argument. Only absolute paths are ever written, so skip anything else rather than pass crashpad_handler a malformed argument built from a corrupted file. setUser and friends were dropped when called before init, because the native annotation list does not exist until then. Replay the stored values once init has run so ordering does not matter. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013vehBwh7WFaVQiawtJa7AN
|
Addressed the three suppressed findings from the latest review, plus the
|
There was a problem hiding this comment.
🟡 Changes recommended
The review found concrete native/bridge issues (JNI UTF string resource handling and attachment removal no-op semantics) that can cause leaks/misleading failures and should be corrected before approval.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
Suppressed comments (2)
Previously missed (2) — in code that hasn't changed since the last review.
app/src/main/cpp/native-lib.cpp:105
- jniInitBugSplat uses GetStringUTFChars() for data_dir/lib_dir/database/application/version but never releases those UTF buffers. Per JNI contract this leaks memory (and may pin the Java strings), and also the wrapper existence check should verify executability (X_OK) rather than just existence (F_OK) to avoid selecting a non-executable wrapper and losing crash reports.
app/src/main/cpp/native-lib.cpp:432 - removeAttachmentPath() always rewrites the attachments list file even when the requested path wasn't attached. If persisting fails (e.g., transient I/O error), this turns what should be a no-op into a reported failure, which contradicts the documented behavior that removing a non-attached path is a no-op.
- Files reviewed: 13/13 changed files
- Comments generated: 1
- Review effort level: Lite
|
|
||
| // The annotation list does not exist until jniInitBugSplat runs, so a | ||
| // setUser/setEmail/setNotes/setKey call made before init was dropped by | ||
| // the native layer. Replay whatever was set so ordering does not matter. | ||
| BugSplat.applyPromotedAttributes(); |
#246) * feat: attach PersistentDataFileAttachmentPaths to native crash reports PersistentDataFileAttachmentPaths reached managed exception reports, feedback and minidumps only. Native crash reports are assembled by the platform's crash reporter, which never sees BugSplat.Attachments, so files configured on the options asset were silently absent from the reports most users expect them on. This was hit during 5.0.0 smoke testing. CreateFromOptions now registers each resolved file with both mechanisms. Startup is the only correct place for the native half. On macOS and iOS a crash report is uploaded at the next launch and BugSplat asks its delegate for attachments then, in a fresh process, so a path registered mid-session is not remembered across the crash and never reaches the report. CreateFromOptions runs on every launch, which is exactly what that model requires. Native registration is compiled out in the editor, so an internal seam records what was resolved and handed to the native reporter, following the existing AutoSubmitCrashReportSetting convention in this file. Refs #242 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G7QwJaDfGEc1sE3sMiCSJp * feat: attach files to native Android crash reports bugsplat-android accepted attachments only while it initialized, so AttachNativeLogFile was a documented no-op there and the paths this branch registers from PersistentDataFileAttachmentPaths reached every native platform except Android. BugSplat-Git/bugsplat-android#26 adds addAttachment and removeAttachment, so the existing registration path now works on Android with no change to how callers use it. Also switches the Android branches from BugSplatBridge to the public BugSplat class and adopts its new setUser, setEmail, setNotes, and setKey rather than promoting the reserved attribute names by hand. The bridge is the JNI surface, not the public API, and hiding it is only possible once nothing outside the AAR calls it. Requires a bugsplat-android build with #26 merged; the vendored AAR must be refreshed before these calls resolve on device. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013vehBwh7WFaVQiawtJa7AN * docs: describe Android attachment behavior Records what the platform actually does: options attachments reach native reports, AttachNativeLogFile now works there, and CapturePlayerLog does nothing because Unity writes no Player.log on Android. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013vehBwh7WFaVQiawtJa7AN * chore: vendor bugsplat-android 1.4.0 Adds addAttachment/removeAttachment and setUser/setEmail/setNotes/setKey, which the Android branches in this PR already call. Until now those calls resolved to methods the bundled AAR did not have and would have thrown on device. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013vehBwh7WFaVQiawtJa7AN * fix: survive an AAR without the attachment API, and qualify the docs Review feedback: AddNativeAttachment and RemoveNativeAttachment called into the AAR with no error handling. CreateFromOptions attaches at startup, so a project that updates the package without the matching bugsplat-android would fail to launch rather than fail to attach. Both now log a warning naming the version needed. NativePersistentDataAttachmentPaths handed out the backing list as an IReadOnlyList, which an in-assembly caller could downcast and mutate. Returns AsReadOnly instead. The docs and changelog said PersistentDataFileAttachmentPaths reaches native crash reports without qualification. It only does so where native crash reporting is enabled; the Android page now names the option. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011bapaeRsoZPuK3jjtdk51V * docs: Android supports native attachments The platform table still said Android was "Not supported - the call is a no-op", and the callout named Windows as the only platform that captures attachments at crash time. Both stopped being true when this branch vendored bugsplat-android 1.4.0. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011bapaeRsoZPuK3jjtdk51V * docs: explain the Apple next-launch attachment model per platform Both Apple pages covered Player.log and hang detection but said nothing about attachments, leaving the next-launch behavior discoverable only by finding it in api.md. Anyone reading the platform page they were sent to would register an attachment mid-session and never learn why it does not arrive. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011bapaeRsoZPuK3jjtdk51V * fix: register persistent attachments before the native reporter starts On macOS and iOS a pending crash report's attachments are gathered synchronously inside -start, once, and persisted with the report. CreateFromOptions registered PersistentDataFileAttachmentPaths after the constructor returned, and the constructor is where start runs, so on Apple those files were absent from every native report. The same ordering mistake #231 fixed for Player.log on iOS. The constructor takes the paths as a parameter now. CreateFromOptions resolves them first and passes them in; the Apple branches hand each one to the bridge before start, and Windows and Android register them after init, which is early enough because both capture at crash time. NativePersistentDataAttachmentPaths still records what was handed over, so the PlayMode tests observe the same thing they did. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011bapaeRsoZPuK3jjtdk51V * fix: guard every Android call and de-duplicate the attachment test seam Review feedback on #246: The six native setters called into the AAR unguarded while the attach and detach paths had their own try/catch. A mismatched AAR would throw from SetNativeUser and friends at startup. One CallAndroid helper now serves every post-init Android call, logging a warning that names the method and the version required. init stays unguarded on purpose: with the class itself missing there is no reporter to degrade to. NativePersistentDataAttachmentPaths recorded every resolved options entry, so a repeated path appeared twice while the native list held it once. It de-duplicates with the same comparer now, and its doc comment says what it records: the paths handed to the constructor, whose native registration happens only where native crash reporting is enabled. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011bapaeRsoZPuK3jjtdk51V --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
… to native crash reports (#246) (#244) * fix: reject absolute PersistentDataFileAttachmentPaths instead of mangling them Entries were stripped of leading separators and combined with Application.persistentDataPath, so `/Users/you/Desktop/test.log` resolved to `<persistentDataPath>/Users/you/Desktop/test.log`. That path never exists, so the file was skipped — and the warning named the mangled path rather than the one that was typed, pointing the reader at a directory they had never heard of. Hit for real during the 5.0.0 smoke test. An absolute entry is now skipped with a warning that quotes it as written and states the persistentDataPath contract, and every warning in the loop names both the entry and the path it resolved to. Rejected rather than accepted: an absolute path belongs to the machine that authored the options asset, which is a ScriptableObject committed to source control and baked into player builds. Accepting it would move the failure from the author's editor — where it is visible — to a teammate's machine, to CI, and to every player's device, where it is not. The sandboxed platforms cannot read outside their own container regardless, so accepting would also work in the editor and fail in the build. Blank rows, which is what Unity leaves behind when you click + on the Inspector list, are now skipped rather than warning about a missing file, and a null entry no longer throws. Closes #241 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KCtsoKMLq2WeYvsPFivVAA * feat: attach PersistentDataFileAttachmentPaths to native crash reports (#246) * feat: attach PersistentDataFileAttachmentPaths to native crash reports PersistentDataFileAttachmentPaths reached managed exception reports, feedback and minidumps only. Native crash reports are assembled by the platform's crash reporter, which never sees BugSplat.Attachments, so files configured on the options asset were silently absent from the reports most users expect them on. This was hit during 5.0.0 smoke testing. CreateFromOptions now registers each resolved file with both mechanisms. Startup is the only correct place for the native half. On macOS and iOS a crash report is uploaded at the next launch and BugSplat asks its delegate for attachments then, in a fresh process, so a path registered mid-session is not remembered across the crash and never reaches the report. CreateFromOptions runs on every launch, which is exactly what that model requires. Native registration is compiled out in the editor, so an internal seam records what was resolved and handed to the native reporter, following the existing AutoSubmitCrashReportSetting convention in this file. Refs #242 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G7QwJaDfGEc1sE3sMiCSJp * feat: attach files to native Android crash reports bugsplat-android accepted attachments only while it initialized, so AttachNativeLogFile was a documented no-op there and the paths this branch registers from PersistentDataFileAttachmentPaths reached every native platform except Android. BugSplat-Git/bugsplat-android#26 adds addAttachment and removeAttachment, so the existing registration path now works on Android with no change to how callers use it. Also switches the Android branches from BugSplatBridge to the public BugSplat class and adopts its new setUser, setEmail, setNotes, and setKey rather than promoting the reserved attribute names by hand. The bridge is the JNI surface, not the public API, and hiding it is only possible once nothing outside the AAR calls it. Requires a bugsplat-android build with #26 merged; the vendored AAR must be refreshed before these calls resolve on device. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013vehBwh7WFaVQiawtJa7AN * docs: describe Android attachment behavior Records what the platform actually does: options attachments reach native reports, AttachNativeLogFile now works there, and CapturePlayerLog does nothing because Unity writes no Player.log on Android. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013vehBwh7WFaVQiawtJa7AN * chore: vendor bugsplat-android 1.4.0 Adds addAttachment/removeAttachment and setUser/setEmail/setNotes/setKey, which the Android branches in this PR already call. Until now those calls resolved to methods the bundled AAR did not have and would have thrown on device. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013vehBwh7WFaVQiawtJa7AN * fix: survive an AAR without the attachment API, and qualify the docs Review feedback: AddNativeAttachment and RemoveNativeAttachment called into the AAR with no error handling. CreateFromOptions attaches at startup, so a project that updates the package without the matching bugsplat-android would fail to launch rather than fail to attach. Both now log a warning naming the version needed. NativePersistentDataAttachmentPaths handed out the backing list as an IReadOnlyList, which an in-assembly caller could downcast and mutate. Returns AsReadOnly instead. The docs and changelog said PersistentDataFileAttachmentPaths reaches native crash reports without qualification. It only does so where native crash reporting is enabled; the Android page now names the option. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011bapaeRsoZPuK3jjtdk51V * docs: Android supports native attachments The platform table still said Android was "Not supported - the call is a no-op", and the callout named Windows as the only platform that captures attachments at crash time. Both stopped being true when this branch vendored bugsplat-android 1.4.0. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011bapaeRsoZPuK3jjtdk51V * docs: explain the Apple next-launch attachment model per platform Both Apple pages covered Player.log and hang detection but said nothing about attachments, leaving the next-launch behavior discoverable only by finding it in api.md. Anyone reading the platform page they were sent to would register an attachment mid-session and never learn why it does not arrive. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011bapaeRsoZPuK3jjtdk51V * fix: register persistent attachments before the native reporter starts On macOS and iOS a pending crash report's attachments are gathered synchronously inside -start, once, and persisted with the report. CreateFromOptions registered PersistentDataFileAttachmentPaths after the constructor returned, and the constructor is where start runs, so on Apple those files were absent from every native report. The same ordering mistake #231 fixed for Player.log on iOS. The constructor takes the paths as a parameter now. CreateFromOptions resolves them first and passes them in; the Apple branches hand each one to the bridge before start, and Windows and Android register them after init, which is early enough because both capture at crash time. NativePersistentDataAttachmentPaths still records what was handed over, so the PlayMode tests observe the same thing they did. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011bapaeRsoZPuK3jjtdk51V * fix: guard every Android call and de-duplicate the attachment test seam Review feedback on #246: The six native setters called into the AAR unguarded while the attach and detach paths had their own try/catch. A mismatched AAR would throw from SetNativeUser and friends at startup. One CallAndroid helper now serves every post-init Android call, logging a warning that names the method and the version required. init stays unguarded on purpose: with the class itself missing there is no reporter to degrade to. NativePersistentDataAttachmentPaths recorded every resolved options entry, so a repeated path appeared twice while the native list held it once. It de-duplicates with the same comparer now, and its doc comment says what it records: the paths handed to the constructor, whose native registration happens only where native crash reporting is enabled. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011bapaeRsoZPuK3jjtdk51V --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: keep managed reporting when native Android init fails, and name the check Review feedback on #244: The Android init called into the AAR unguarded. With the class missing or mismatched the exception left the constructor before UseDotNetHandler ran, so a broken native setup silently took managed exception reporting down with it. The init is wrapped now: an error names the AAR requirement, native reporting stays off, and the managed handler installs as before. The rooted-path warning said "absolute". Path.IsPathRooted also accepts the Windows drive-relative form, so it now says "not a relative path", which is the actual condition. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011bapaeRsoZPuK3jjtdk51V * docs: make the PersistentDataFileAttachmentPaths tooltip match api.md "Upload with each report" was the vagueness #242 complained about. The Inspector now states the actual rule the API reference already gives: managed reports always, native crash reports where native reporting is enabled, absolute paths skipped with a warning. Refs #242 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011bapaeRsoZPuK3jjtdk51V * docs: match the tooltip to the rooted-path check it describes The tooltip said absolute paths are skipped, but the check is Path.IsPathRooted, which also catches the Windows drive-relative form. The warning was corrected in da458fe; this brings the Inspector with it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011bapaeRsoZPuK3jjtdk51V * fix: refuse attachment paths that escape persistentDataPath Rooted entries were already rejected, but Path.Combine resolves "../outside.log" to a sibling of persistentDataPath, so a relative-looking entry could still name a file outside it. That is the same defect this branch exists to fix: an entry that does not mean what the field says, works on the authoring machine, and silently attaches nothing on the sandboxed platforms. A resolved path outside the root is skipped with a warning naming both the entry and where it resolved to, using the same case rule as the native attachment comparer. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011bapaeRsoZPuK3jjtdk51V --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Closes #25
Summary
BugSplat.addAttachment(path)/BugSplat.removeAttachment(path)so crash attachments can be updated afterinit, matchingsetAttribute/removeAttribute.--attachmentpaths into the handler argv atStartHandlerAtCrashand never rereads them. A small PIE wrapper (libbugsplat_handler.so) is exec'd instead: it loads<database>/bugsplat_attachments.listand execslibcrashpad_handler.sowith the current set.Also in this PR: promoted-field setters
Added alongside the attachment work because both close the same gap — bugsplat-unity could not reach either capability, and shipping them separately would mean two AAR releases for one Unity release.
Callers previously had to know the reserved attribute names the backend promotes (
BugSplatUser,BugSplatEmail,BugSplatNotes,BugSplatApplicationKey) and set them throughsetAttribute, which couples clients to a naming convention this SDK does not document.BugSplatand applied to the commit request as typedCommitOptionsfields —user,email,notes,appKey— which ANR reports previously never carried.initare replayed once the native annotation list exists, so call order does not matter.Test plan
./gradlew :app:testDebugUnitTest— including newAttachmentPathTest(null/blank/newline rejection)./gradlew :app:assembleDebug— wrapper packaged in the AAR for arm64-v8a, armeabi-v7a, and x86_640x4000) LOAD alignment:example:compileDebugJavaWithJavac); demo attachesapp.logafter initaddAttachmentof a log written mid-session, crash, confirm the file on the BugSplat dashboardremoveAttachment, crash, confirm the file is absent