Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added

- Docs: on IL2CPP, frames before an `--- End of stack trace from previous location where exception was thrown ---` separator (an exception thrown in an async continuation and rethrown through await) carry no address and, with method names only, stay unresolved (name only, `(<0000..>:0)`); only the frames after the separator resolve. By design: the earlier part's native addresses are not available at report time and a wrong address would give a wrong file:line. Seen on staging IUTA-80 (`fx-async`, iOS, Unity 2022.3.62f3, Release IL2CPP: 6 frames before the separator unresolved, 12 after resolved); the dashboard result of this shape was observed on iOS only, not on Android; with IL2CPP line information the text carries file:line and this does not apply. Also documented: an `async void` entry stub frame can resolve to a neighbouring method's line until a backend line-lookup fix. No code change.
- Documentation: managed exceptions the SDK cannot see. An exception thrown in a `ThreadPool` work item (`ThreadPool.QueueUserWorkItem`) on the Mono backend is lost without any SDK involvement (Unity logs nothing, no `AppDomain.UnhandledException`, the process keeps running; reproduced without the Bugsee package on an Android ARMv7 phone, Unity 2021.3.45f1); IL2CPP (iOS, Android ARM64) raises `AppDomain.UnhandledException` and is reported. Catch inside the work item and call `Bugsee.LogException`. `Task.Run` and a dropped `async Task` are reported only when the runtime finalizes the faulted task (`UnobservedTaskException`), so late, or missing if the process exits first (4 of 18 did not raise `UnobservedTaskException` within 16 s of the throw, with forced GC at 8 s and 11 s; the event can arrive tens of seconds after a collection); await tasks or handle faults. Lists the hooks the SDK registers and what was and was not measured. No code changed.
- Tests (addresses #206): `Tests~/iOS/BridgeExportTests.mm` now checks the bridge's hand-offs to the SDK by swapping the SDK's class methods for recorders: enabling the network, log and breadcrumb filters hands the SDK a filter block (disabling does not call it), and `_bugsee_upload_managed_report` asks the SDK for one report, applies the JSON's fields, reports success and uploads that same report (no report, or a fence made stale by an invalidation, fails and uploads nothing; an invalidation landing while the fields are applied fails it, one landing from the success callback stops the upload); the filter check runs the block it was handed to prove it is the one for that kind. No production code changed. Still not asserted, by necessity: the email round trip on a bare simulator (no keychain entitlement; the device scenarios cover it), and `_bugsee_test_crash` (kills the process).
- Tests (addresses #141): `AndroidFakeJniTests` compiles the real `AndroidJavaConverters`, `AndroidOptionsMapper` and `AndroidExchangeEvents` sources on the host (Roslyn, `UNITY_ANDROID` defined, a stand-in for `AndroidJavaObject` / `AndroidJavaClass`) and runs them against a fake Java object model: boxing and unboxing of String, Boolean, Integer, Long, Double, Byte, Short, Character and an SDK-supplied Float (a float set by the app goes out as a Double), attribute widening, attribute and header map round trips, option enum coercion with the SDK's fallback value, filter event getters and setters (network, log, breadcrumb), the stage and level mappings and the release guard. The stand-in enforces the JNI signature rules (argument and return types must match, no use after `Dispose`). No production code changed. Not covered: the `CallbackProxies` (filter, report handler and lifecycle proxies), `AndroidReport`, `AndroidAttachment`, appearance and feedback, which need Unity's main-thread dispatcher or more of the Unity API than the stand-in has.
- Sample scenario `vhsecure` (addresses #229): a manual report taken under a secure rectangle over the top half of the screen, so the stored managed `viewtree` of the report can be checked on the dashboard, in development and release builds on both platforms (the report is the probe, not the development-only log line). Checked on staging on an Android phone (Unity 6000.0.84f1) and an iPhone XS (Unity 2022.3.62f3): every report's `viewtree` carries `managed` as a JSON string next to `native`; in the secured reports (762 of 1394 and 856 of 1500 nodes secured) every on-screen node overlapping the covered half has no name or component list, and a node with no placeable bounds loses them too whenever any rectangle is registered. The trees were truncated at the 1500-node cap and no specific bottom-half node was confirmed to keep its name. Whether the viewer draws it was not checked.
Expand Down
27 changes: 27 additions & 0 deletions Documentation~/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,33 @@ public override void OnBeforeReportCreated(IReport report, bool isTerminating, A

Managed exceptions use the **UnityManagedException** JSON-in-reason contract (`name`, `reason`, `frames[{trace,address?}]`, `signature`, `buildID`, `moduleUUID`, optional nested `cause`). The nested `cause` reaches the SDK and the server inside the reason text, but the worker does not read it yet (see the managed-stack notes below): until it does, only the outermost exception is shown, symbolicated and grouped; the inner exceptions are not displayed.

### Managed exceptions the SDK cannot see (ThreadPool on Mono, tasks)

Managed capture (`CaptureManagedExceptions`) registers these hooks, during launch (`Runtime/Internal/ExceptionPipeline.cs`):

- a wrapper around `Debug.unityLogger.logHandler` (its `LogException`);
- `Application.logMessageReceivedThreaded`, as a fallback for exceptions Unity logs without going through the handler (an echo of an exception already reported is dropped);
- `AppDomain.CurrentDomain.UnhandledException`;
- `TaskScheduler.UnobservedTaskException` (not on `NET_LEGACY` profiles; `CaptureUnobservedTaskExceptions`).

There is nothing else (no `FirstChanceException`). An exception that reaches none of these is not reported.

**An exception thrown in a `ThreadPool` work item on the Mono backend is lost** (measured on Unity 2021.3.45f1, Android ARMv7; other Unity versions not measured). With `ThreadPool.QueueUserWorkItem(_ => throw ...)` on Android Mono (ARMv7), Unity logs nothing, `AppDomain.UnhandledException` is not raised and the process keeps running, so no managed hook can see it and the SDK reports nothing. This does not involve the SDK: it was reproduced in a player built without the Bugsee package. On IL2CPP the same code raises `AppDomain.UnhandledException` and the SDK reports it (twice, as a handled and a crash-type event, because Unity also logs it). A `new Thread` that throws is logged by Unity and reported on both backends. Workaround: catch inside the work item and report it yourself:

```csharp
ThreadPool.QueueUserWorkItem(_ =>
{
try { DoWork(); }
catch (Exception e) { Bugsee.LogException(e); }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Severity: P2 Medium
Location: Documentation~/index.md:209 (same call in the ContinueWith(..., OnlyOnFaulted) sentence at :213)
Problem: The workaround this section exists to teach is catch + Bugsee.LogException inside ThreadPool.QueueUserWorkItem. On Android that is a public JNI call with no attach. AndroidBridge.SendManagedPayload builds a UnityManagedException and calls Bugsee.logException on the calling thread and does not enter AndroidJniScope. Span APIs and GetLaunched do, because this same file already states the JVM “aborts the process on a JNI call from an unattached thread” (Documentation~/index.md:683; JniAttachPolicy / AndroidJniScope comments: CheckJNI “use of deleted local reference”, Galaxy Tab A 8.0). ExceptionPipeline.DispatchPayload hops handled auto-capture onto the Unity main thread before the same JNI; the public Bugsee.LogException path does not. Sample SignatureThreadsE2E only calls Bugsee.LogException from a worker after AndroidJNI.AttachCurrentThread().
Impact: On the configuration this paragraph measured (Android Mono ARMv7), following the snippet can replace a silently lost exception (process keeps running) with a hard abort. ContinueWith(..., OnlyOnFaulted) plus Bugsee.LogException is the same pool-thread JNI call.
Scenario: Copy the snippet onto the Honor / Unity 2021.3.45f1 Mono player. DoWork() throws, catch runs on a CLR thread-pool thread (IsThreadPoolThread true, the case JniAttachPolicy treats as AttachDetach). Bugsee.LogException → SendManagedPayload → new AndroidJavaObject(...) with no JNIEnv. Auto-capture of UnobservedTaskException would have been safe because it is handled and MainThreadDispatcher.Runs the send; this snippet bypasses that hop.
Fix: Point the snippet at a path that is already safe from a background thread: Debug.LogException(e) (CHANGELOG: background Debug.LogException now reports; the pipeline hops handled sends to main). Or keep Bugsee.LogException and say it must run on the Unity main thread, or after AndroidJNI.AttachCurrentThread() (and detach a pool thread), matching sigthreads. The durable product fix is using (AndroidJniScope.Enter()) around SendManagedPayload, which this docs-only PR does not have to include.

});
```

**`Task.Run` and an `async Task` whose result is dropped** store the exception in the `Task`; nothing is logged. The SDK reports it only when the runtime finalizes the faulted task and raises `TaskScheduler.UnobservedTaskException` (as a handled error with the stack of the throw). That depends on garbage collection, so the report can arrive late, or not at all if the app exits first or no collection finalizes the task. Await your tasks, or handle faults (`ContinueWith(..., TaskContinuationOptions.OnlyOnFaulted)`), and report them with `Bugsee.LogException`. `async void` methods that run on the Unity main thread and coroutines are logged by Unity and reported normally (measured). An `async void` method started from a `ThreadPool` work item on Mono was not measured; expect it to be lost like a synchronous throw there, so catch and report inside it.

What was measured: Unity 2021.3.45f1, a physical Honor WOD-LX1 (Android 15; Mono ARMv7 and IL2CPP ARM64, Release builds, and Development for Mono) and an iPhone XS (iOS 18.7.9, IL2CPP Release), each with and without the SDK where noted above. The same probe also threw from a bare `new Thread`, an `async void` method that awaits a faulting task and a coroutine: Unity logged each on every backend tested, and with the SDK each was reported (on the iPhone and the Honor Mono Release build). The pool loss was seen on both Mono Release and Development without the SDK, and in a Release build with it. With a forced `GC.Collect()` plus `GC.WaitForPendingFinalizers()` (at 8 s and again at 11 s after the throw, observed until 16 s; no longer wait, and the event is known to be able to arrive tens of seconds after a collection), 4 of 18 task cases did not raise `UnobservedTaskException` in that window (Mono Release `Task.Run`; IL2CPP Android `Task.Run` and an awaited-then-dropped `async Task`; iOS an awaited-then-dropped `async Task`, without the SDK). Not measured: Windows or desktop players, other Unity versions, other devices.

Why the pool case is silent on Mono is taken from knowledge of the Mono runtime source, not verified on symbols of this build: the managed pool dispatch loop has no catch-all, the native pool worker invokes the work item with a call that catches the exception, and Unity sets Mono's legacy unhandled-exception policy, under which that path does not raise `AppDomain.UnhandledException`. A `new Thread` has no catching frame, so the event is raised before the policy is consulted.
Comment thread
krassx marked this conversation as resolved.
Resolved

### How to get C# class + file:line on `LogException`

| Mode | Build / capture | Backend | File:line? |
Expand Down
Loading