diff --git a/CHANGELOG.md b/CHANGELOG.md index 3373773..e921b31 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/Documentation~/index.md b/Documentation~/index.md index 3bff3a1..c6b5f0e 100644 --- a/Documentation~/index.md +++ b/Documentation~/index.md @@ -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); } +}); +``` + +**`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. + ### How to get C# class + file:line on `LogException` | Mode | Build / capture | Backend | File:line? |