<|start_filename|>src/Sentry.Unity/UnityLogException.cs<|end_filename|>
using System;
using System.Collections.Generic;
using System.IO;
using Sentry.Protocol;
using UnityEngine;

namespace Sentry.Unity
{
    /// <summary>
    /// An exception raised through the Unity logging callback
    /// </summary>
    /// <remarks>
    /// <see cref="Application.logMessageReceived"/>
    /// </remarks>
    internal class UnityLogException : Exception
    {
        public string LogString { get; }
        public string LogStackTrace { get; }

        public UnityLogException(string logString, string logStackTrace)
        {
            LogString = logString;
            LogStackTrace = logStackTrace;
            Data[Mechanism.MechanismKey] = "unity.log";
        }

        private UnityLogException() : base()
        {
            LogString = "";
            LogStackTrace = "";
        }

        private UnityLogException(string message) : base(message)
        {
            LogString = "";
            LogStackTrace = "";
        }

        private UnityLogException(string message, Exception innerException) : base(message, innerException)
        {
            LogString = "";
            LogStackTrace = "";
        }

        public SentryException ToSentryException()
        {
            var frames = new List<SentryStackFrame>();
            var exc = LogString.Split(new char[] { ':' }, 2);
            var excType = exc[0];
            // TODO: condition may NOT contain ':' separator
            var excValue = exc.Length == 1 ? exc[0] : exc[1].Substring(1); // strip the space
            var stackList = LogStackTrace.Split('\n');

            // The format is as follows:
            // Module.Class.Method[.Invoke] (arguments) (at filename:lineno)
            // where :lineno is optional, will be ommitted in builds
            for (var i = 0; i < stackList.Length; i++)
            {
                string functionName;
                string filename;
                int lineNo;

                var item = stackList[i].TrimEnd('\r');
                if (item == string.Empty)
                {
                    continue;
                }

                var closingParen = item.IndexOf(')');

                if (closingParen == -1)
                {
                    functionName = item;
                    lineNo = -1;
                    filename = string.Empty;
                }
                else
                {
                    try
                    {
                        functionName = item.Substring(0, closingParen + 1);
                        if (item.Substring(closingParen + 1, 5) != " (at ")
                        {
                            // we did something wrong, failed the check
                            Debug.Log("failed parsing " + item);
                            functionName = item;
                            lineNo = -1;
                            filename = string.Empty;
                        }
                        else
                        {
                            var colon = item.LastIndexOf(':', item.Length - 1, item.Length - closingParen);
                            if (closingParen == item.Length - 1)
                            {
                                filename = string.Empty;
                                lineNo = -1;
                            }
                            else if (colon == -1)
                            {
                                filename = item.Substring(closingParen + 6, item.Length - closingParen - 7);
                                lineNo = -1;
                            }
                            else
                            {
                                filename = item.Substring(closingParen + 6, colon - closingParen - 6);
                                lineNo = Convert.ToInt32(item.Substring(colon + 1, item.Length - 2 - colon));
                            }
                        }
                    }
                    catch (Exception)
                    {
                        functionName = item;
                        lineNo = -1;
                        filename = string.Empty; // we have no clue
                    }
                }

                var filenameWithoutZeroes = StripZeroes(filename);
                frames.Add(new SentryStackFrame
                {
                    FileName = TryResolveFileNameForMono(filenameWithoutZeroes),
                    AbsolutePath = filenameWithoutZeroes,
                    Function = functionName,
                    LineNumber = lineNo == -1 ? null : lineNo,
                    InApp = functionName != null
                        && !functionName.StartsWith("UnityEngine", StringComparison.Ordinal)
                        && !functionName.StartsWith("System", StringComparison.Ordinal)
                });
            }

            frames.Reverse();

            var stacktrace = new SentryStackTrace();
            foreach (var frame in frames)
            {
                stacktrace.Frames.Add(frame);
            }

            return new SentryException
            {
                Stacktrace = stacktrace,
                Type = excType,
                Value = excValue
            };
        }

        // https://github.com/getsentry/sentry-unity/issues/103
        private static string StripZeroes(string filename)
            => filename.Equals("<00000000000000000000000000000000>", StringComparison.OrdinalIgnoreCase)
                ? string.Empty
                : filename;

        // TODO: discuss
        private static string TryResolveFileNameForMono(string fileName)
        {
            try
            {
                // throws on Mono for <1231231231> paths
                return Path.GetFileName(fileName);
            }
            catch
            {
                // mono path
                return "Unknown";
            }
        }
    }
}



<|start_filename|>src/Sentry.Unity.Editor/SentryPreprocessBuild.cs<|end_filename|>
using UnityEditor;
using UnityEditor.Build;
using UnityEditor.Build.Reporting;

namespace Sentry.Unity.Editor
{
    public class SentryPreprocessBuild : IPreprocessBuildWithReport
    {
        public int callbackOrder { get; private set; }

        public void OnPreprocessBuild(BuildReport report)
        {
            // var options = AssetDatabase.LoadAssetAtPath<SentryCliOptions>(SentryWindows.SentryCliOptionsAssetPath);

            switch (report.summary.platform)
            {
                case BuildTarget.StandaloneOSX:
                case BuildTarget.StandaloneLinux64:
                case BuildTarget.StandaloneWindows:
                case BuildTarget.StandaloneWindows64:
                case BuildTarget.tvOS:
                case BuildTarget.iOS:
                case BuildTarget.XboxOne: // pdbs?
                    // platformGroup = Standalone
                    // sentry-cli upload-dif
                    break;
                case BuildTarget.WebGL:
                    // sentry-cli sourcemaps
                    break;
                case BuildTarget.Android:
                    // sentry-cli proguard
                    break;
                default:
                    // nothing to do
                    break;
            }
            // file: summary.outputPath
            // guid: summary.guid
        }
    }
}


<|start_filename|>test/Sentry.Unity.Tests/Stubs/TestHub.cs<|end_filename|>
using System;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace Sentry.Unity.Tests.Stubs
{
    internal sealed class TestHub : IHub
    {
        private readonly List<SentryEvent> _capturedEvents = new();
        private readonly List<Action<Scope>> _configureScopeCalls = new();

        public IReadOnlyList<SentryEvent> CapturedEvents => _capturedEvents;
        public IReadOnlyList<Action<Scope>> ConfigureScopeCalls => _configureScopeCalls;

        public TestHub(bool isEnabled = true)
        {
            IsEnabled = isEnabled;
        }
        public bool IsEnabled { get; }

        public SentryId CaptureEvent(SentryEvent evt, Scope? scope = null)
        {
            _capturedEvents.Add(evt);
            return evt.EventId;
        }

        public void CaptureUserFeedback(UserFeedback userFeedback)
        {
            throw new NotImplementedException();
        }

        public void CaptureTransaction(Transaction transaction)
        {
        }

        public void CaptureSession(SessionUpdate sessionUpdate)
        {
        }

        public Task FlushAsync(TimeSpan timeout)
        {
            return Task.CompletedTask;
        }

        public void ConfigureScope(Action<Scope> configureScope) => _configureScopeCalls.Add(configureScope);

        public Task ConfigureScopeAsync(Func<Scope, Task> configureScope) => Task.CompletedTask;

        public void BindClient(ISentryClient client)
        {
            throw new NotImplementedException();
        }

        public IDisposable PushScope()
        {
            throw new NotImplementedException();
        }

        public IDisposable PushScope<TState>(TState state)
        {
            throw new NotImplementedException();
        }

        public void WithScope(Action<Scope> scopeCallback)
        {
            throw new NotImplementedException();
        }

        public SentryId LastEventId { get; }
        public ITransaction StartTransaction(ITransactionContext context, IReadOnlyDictionary<string, object?> customSamplingContext)
        {
            throw new NotImplementedException();
        }

        public void BindException(Exception exception, ISpan span)
        {
            throw new NotImplementedException();
        }

        public ISpan? GetSpan()
        {
            throw new NotImplementedException();
        }

        public SentryTraceHeader? GetTraceHeader()
        {
            throw new NotImplementedException();
        }

        public void StartSession()
        {
            // TODO: test sessions
        }

        public void PauseSession()
        {
            // TODO: test sessions
        }

        public void ResumeSession()
        {
            // TODO: test sessions
        }

        public void EndSession(SessionEndStatus status = SessionEndStatus.Exited)
        {
            // TODO: test sessions
        }
    }
}


<|start_filename|>src/Sentry.Unity.Editor/SentryTestWindow.cs<|end_filename|>
using System;
using System.IO;
using UnityEditor;

namespace Sentry.Unity.Editor
{
    public sealed class SentryTestWindow : SentryWindow, IDisposable
    {
        protected override string SentryOptionsAssetName { get; } = Path.GetRandomFileName();

        public static SentryTestWindow Open()
            => (SentryTestWindow)GetWindow(typeof(SentryTestWindow));

        public void Dispose()
        {
            Close(); // calls 'OnLostFocus' implicitly
            AssetDatabase.DeleteAsset(ScriptableSentryUnityOptions.GetConfigPath(SentryOptionsAssetName));
        }
    }
}


<|start_filename|>samples/unity-of-bugs/Assets/Scripts/AdditionalButtons.cs<|end_filename|>
﻿using System.Collections;
using System.Threading;
using System.Threading.Tasks;
using Sentry;
using UnityEngine;
using UnityEngine.SceneManagement;

public class AdditionalButtons : MonoBehaviour
{
    public void SetUser()
    {
        SentrySdk.ConfigureScope(s =>
        {
            s.User = new User
            {
                Email = "<EMAIL>",
                Username = "ant",
                Id = "ant-id"
            };
        });
        Debug.Log("User set: ant");
    }

    class PlayerCharacter
    {
        public string Name { get; set; }
        public int Age { get; set; }
        public string AttackType { get; set; }
    }

    public void CaptureMessageWithContext()
    {
        SentrySdk.ConfigureScope(scope =>
        {
            scope.Contexts["character"] = new PlayerCharacter
            {
                Name = "<NAME>",
                Age = 19,
                AttackType = "melee"
            };
        });

        SentrySdk.CaptureMessage("Capturing with player character context.");
        SentrySdk.ConfigureScope(scope => scope.Contexts = null);
    }

    public void BackgroundBreadcrumb() =>
        Task.Run(() => SentrySdk.AddBreadcrumb("Breadcrumb from the background", "background task"));

    public void CaptureMessageWithScreenshot() => StartCoroutine(CaptureScreenshot());

    private IEnumerator CaptureScreenshot()
    {
        yield return new WaitForEndOfFrame();
        SentrySdk.ConfigureScope(s =>
        {
            var screenshot = ScreenCapture.CaptureScreenshotAsTexture();
            s.AddAttachment(screenshot.EncodeToJPG(), "screenshot.jpg");
        });

        SentrySdk.CaptureMessage("Captured a message with a screenshot attachment");
        SentrySdk.ConfigureScope(scope => scope.ClearAttachments());
    }

    public void LoadBugfarm() => SceneManager.LoadScene("1_Bugfarm");
    public void LoadMobileNativeSupport() => SceneManager.LoadScene("2_MobileNativeSupport");
}


<|start_filename|>test/Sentry.Unity.Tests/UnityLogExceptionTests.cs<|end_filename|>
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using Sentry.Protocol;

namespace Sentry.Unity.Tests
{
    public class UnityLogExceptionTests
    {
        [TestCaseSource(nameof(ParsingTestCases))]
        public void ToSentryException_ParsingTestCases(
            string logString,
            string logStackTrace,
            SentryException sentryException)
        {
            var actual = new UnityLogException(logString, logStackTrace).ToSentryException();

            AssertEqual(sentryException, actual);
        }

        private static readonly object[] ParsingTestCases =
        {
            new object[] {
                "NullReferenceException: Object reference not set to an instance of an object",
                @"BugFarm.ThrowNull () (at Assets/Scripts/BugFarm.cs:33)
UnityEngine.Events.InvokableCall.Invoke () (at /Users/bokken/buildslave/unity/build/Runtime/Export/UnityEvent/UnityEvent.cs:166)
UnityEngine.Events.UnityEvent.Invoke () (at /Users/bokken/buildslave/unity/build/Runtime/Export/UnityEvent/UnityEvent/UnityEvent_0.cs:58)
UnityEngine.UI.Button.Press () (at /Applications/Unity/Hub/Editor/2019.4.21f1/Unity.app/Contents/Resources/PackageManager/BuiltInPackages/com.unity.ugui/Runtime/UI/Core/Button.cs:68)
UnityEngine.UI.Button.OnPointerClick (UnityEngine.EventSystems.PointerEventData eventData) (at /Applications/Unity/Hub/Editor/2019.4.21f1/Unity.app/Contents/Resources/PackageManager/BuiltInPackages/com.unity.ugui/Runtime/UI/Core/Button.cs:110)
UnityEngine.EventSystems.ExecuteEvents.Execute (UnityEngine.EventSystems.IPointerClickHandler handler, UnityEngine.EventSystems.BaseEventData eventData) (at /Applications/Unity/Hub/Editor/2019.4.21f1/Unity.app/Contents/Resources/PackageManager/BuiltInPackages/com.unity.ugui/Runtime/EventSystem/ExecuteEvents.cs:50)
UnityEngine.EventSystems.ExecuteEvents.Execute[T] (UnityEngine.GameObject target, UnityEngine.EventSystems.BaseEventData eventData, UnityEngine.EventSystems.ExecuteEvents+EventFunction`1[T1] functor) (at /Applications/Unity/Hub/Editor/2019.4.21f1/Unity.app/Contents/Resources/PackageManager/BuiltInPackages/com.unity.ugui/Runtime/EventSystem/ExecuteEvents.cs:261)
UnityEngine.EventSystems.EventSystem:Update() (at /Applications/Unity/Hub/Editor/2019.4.21f1/Unity.app/Contents/Resources/PackageManager/BuiltInPackages/com.unity.ugui/Runtime/EventSystem/EventSystem.cs:377)",
                new SentryException
                {
                    Value = "Object reference not set to an instance of an object",
                    Type = "NullReferenceException",
                    Stacktrace = new SentryStackTrace
                    {
                        Frames = new List<SentryStackFrame>
                        {
                            new()
                            {
                                Function = "UnityEngine.EventSystems.EventSystem:Update()",
                                AbsolutePath = "/Applications/Unity/Hub/Editor/2019.4.21f1/Unity.app/Contents/Resources/PackageManager/BuiltInPackages/com.unity.ugui/Runtime/EventSystem/EventSystem.cs",
                                LineNumber = 377,
                                FileName = "EventSystem.cs",
                                InApp = false
                            },
                            new()
                            {
                                Function = "UnityEngine.EventSystems.ExecuteEvents.Execute[T] (UnityEngine.GameObject target, UnityEngine.EventSystems.BaseEventData eventData, UnityEngine.EventSystems.ExecuteEvents+EventFunction`1[T1] functor)",
                                AbsolutePath = "/Applications/Unity/Hub/Editor/2019.4.21f1/Unity.app/Contents/Resources/PackageManager/BuiltInPackages/com.unity.ugui/Runtime/EventSystem/ExecuteEvents.cs",
                                LineNumber = 261,
                                FileName = "ExecuteEvents.cs",
                                InApp = false
                            },
                            new()
                            {
                                Function = "UnityEngine.EventSystems.ExecuteEvents.Execute (UnityEngine.EventSystems.IPointerClickHandler handler, UnityEngine.EventSystems.BaseEventData eventData)",
                                AbsolutePath = "/Applications/Unity/Hub/Editor/2019.4.21f1/Unity.app/Contents/Resources/PackageManager/BuiltInPackages/com.unity.ugui/Runtime/EventSystem/ExecuteEvents.cs",
                                LineNumber = 50,
                                FileName = "ExecuteEvents.cs",
                                InApp = false
                            },
                            new()
                            {
                                Function = "UnityEngine.UI.Button.OnPointerClick (UnityEngine.EventSystems.PointerEventData eventData)",
                                AbsolutePath = "/Applications/Unity/Hub/Editor/2019.4.21f1/Unity.app/Contents/Resources/PackageManager/BuiltInPackages/com.unity.ugui/Runtime/UI/Core/Button.cs",
                                LineNumber = 110,
                                FileName = "Button.cs",
                                InApp = false
                            },
                            new()
                            {
                                Function = "UnityEngine.UI.Button.Press ()",
                                AbsolutePath = "/Applications/Unity/Hub/Editor/2019.4.21f1/Unity.app/Contents/Resources/PackageManager/BuiltInPackages/com.unity.ugui/Runtime/UI/Core/Button.cs",
                                LineNumber = 68,
                                FileName = "Button.cs",
                                InApp = false
                            },
                            new()
                            {
                                Function = "UnityEngine.Events.UnityEvent.Invoke ()",
                                AbsolutePath = "/Users/bokken/buildslave/unity/build/Runtime/Export/UnityEvent/UnityEvent/UnityEvent_0.cs",
                                LineNumber = 58,
                                FileName = "UnityEvent_0.cs",
                                InApp = false
                            },
                            new()
                            {
                                Function = "UnityEngine.Events.InvokableCall.Invoke ()",
                                AbsolutePath = "/Users/bokken/buildslave/unity/build/Runtime/Export/UnityEvent/UnityEvent.cs",
                                LineNumber = 166,
                                FileName = "UnityEvent.cs",
                                InApp = false
                            },
                            new()
                            {
                                Function = "BugFarm.ThrowNull ()",
                                AbsolutePath = "Assets/Scripts/BugFarm.cs",
                                LineNumber = 33,
                                FileName = "BugFarm.cs",
                                InApp = true
                            },
                        }
                    }
                } },
            new object[] {
                @"AssertionException: Assertion failure. Values are not equal.
Expected: False == True",
                @"UnityEngine.Assertions.Assert.Fail (System.String message, System.String userMessage) (at /Users/bokken/buildslave/unity/build/Runtime/Export/Assertions/Assert/AssertBase.cs:29)
UnityEngine.Assertions.Assert.AreEqual[T] (T expected, T actual, System.String message, System.Collections.Generic.IEqualityComparer`1[T] comparer) (at /Users/bokken/buildslave/unity/build/Runtime/Export/Assertions/Assert/AssertGeneric.cs:31)
UnityEngine.Assertions.Assert.AreEqual[T] (T expected, T actual, System.String message) (at /Users/bokken/buildslave/unity/build/Runtime/Export/Assertions/Assert/AssertGeneric.cs:19)
UnityEngine.Assertions.Assert.AreEqual[T] (T expected, T actual) (at /Users/bokken/buildslave/unity/build/Runtime/Export/Assertions/Assert/AssertGeneric.cs:13)
BugFarm.AssertFalse () (at Assets/Scripts/BugFarm.cs:28)
UnityEngine.Events.InvokableCall.Invoke () (at /Users/bokken/buildslave/unity/build/Runtime/Export/UnityEvent/UnityEvent.cs:166)
UnityEngine.Events.UnityEvent.Invoke () (at /Users/bokken/buildslave/unity/build/Runtime/Export/UnityEvent/UnityEvent/UnityEvent_0.cs:58)
UnityEngine.UI.Button.Press () (at /Applications/Unity/Hub/Editor/2019.4.21f1/Unity.app/Contents/Resources/PackageManager/BuiltInPackages/com.unity.ugui/Runtime/UI/Core/Button.cs:68)
UnityEngine.UI.Button.OnPointerClick (UnityEngine.EventSystems.PointerEventData eventData) (at /Applications/Unity/Hub/Editor/2019.4.21f1/Unity.app/Contents/Resources/PackageManager/BuiltInPackages/com.unity.ugui/Runtime/UI/Core/Button.cs:110)
UnityEngine.EventSystems.ExecuteEvents.Execute (UnityEngine.EventSystems.IPointerClickHandler handler, UnityEngine.EventSystems.BaseEventData eventData) (at /Applications/Unity/Hub/Editor/2019.4.21f1/Unity.app/Contents/Resources/PackageManager/BuiltInPackages/com.unity.ugui/Runtime/EventSystem/ExecuteEvents.cs:50)
UnityEngine.EventSystems.ExecuteEvents.Execute[T] (UnityEngine.GameObject target, UnityEngine.EventSystems.BaseEventData eventData, UnityEngine.EventSystems.ExecuteEvents+EventFunction`1[T1] functor) (at /Applications/Unity/Hub/Editor/2019.4.21f1/Unity.app/Contents/Resources/PackageManager/BuiltInPackages/com.unity.ugui/Runtime/EventSystem/ExecuteEvents.cs:261)
Class.Method () (at /Applications/Test/Fake.cs)
UnityEngine.EventSystems.EventSystem:Update() (at /Applications/Unity/Hub/Editor/2019.4.21f1/Unity.app/Contents/Resources/PackageManager/BuiltInPackages/com.unity.ugui/Runtime/EventSystem/EventSystem.cs:377)",
                new SentryException
                {
                    Value = @"Assertion failure. Values are not equal.
Expected: False == True",
                    Type = "AssertionException",
                    Stacktrace = new SentryStackTrace
                    {
                        Frames = new List<SentryStackFrame>
                        {
                            new()
                            {
                                Function = "UnityEngine.EventSystems.EventSystem:Update()",
                                AbsolutePath = "/Applications/Unity/Hub/Editor/2019.4.21f1/Unity.app/Contents/Resources/PackageManager/BuiltInPackages/com.unity.ugui/Runtime/EventSystem/EventSystem.cs",
                                LineNumber = 377,
                                FileName = "EventSystem.cs",
                                InApp = false
                            },
                            new()
                            {
                                Function = "Class.Method ()",
                                AbsolutePath = "/Applications/Test/Fake.cs",
                                // Missing line number
                                LineNumber = null,
                                FileName = "Fake.cs",
                                InApp = true
                            },
                            new()
                            {
                                Function = "UnityEngine.EventSystems.ExecuteEvents.Execute[T] (UnityEngine.GameObject target, UnityEngine.EventSystems.BaseEventData eventData, UnityEngine.EventSystems.ExecuteEvents+EventFunction`1[T1] functor)",
                                AbsolutePath = "/Applications/Unity/Hub/Editor/2019.4.21f1/Unity.app/Contents/Resources/PackageManager/BuiltInPackages/com.unity.ugui/Runtime/EventSystem/ExecuteEvents.cs",
                                LineNumber = 261,
                                FileName = "ExecuteEvents.cs",
                                InApp = false
                            },
                            new()
                            {
                                Function = "UnityEngine.EventSystems.ExecuteEvents.Execute (UnityEngine.EventSystems.IPointerClickHandler handler, UnityEngine.EventSystems.BaseEventData eventData)",
                                AbsolutePath = "/Applications/Unity/Hub/Editor/2019.4.21f1/Unity.app/Contents/Resources/PackageManager/BuiltInPackages/com.unity.ugui/Runtime/EventSystem/ExecuteEvents.cs",
                                LineNumber = 50,
                                FileName = "ExecuteEvents.cs",
                                InApp = false
                            },
                            new()
                            {
                                Function = "UnityEngine.UI.Button.OnPointerClick (UnityEngine.EventSystems.PointerEventData eventData)",
                                AbsolutePath = "/Applications/Unity/Hub/Editor/2019.4.21f1/Unity.app/Contents/Resources/PackageManager/BuiltInPackages/com.unity.ugui/Runtime/UI/Core/Button.cs",
                                LineNumber = 110,
                                FileName = "Button.cs",
                                InApp = false
                            },
                            new()
                            {
                                Function = "UnityEngine.UI.Button.Press ()",
                                AbsolutePath = "/Applications/Unity/Hub/Editor/2019.4.21f1/Unity.app/Contents/Resources/PackageManager/BuiltInPackages/com.unity.ugui/Runtime/UI/Core/Button.cs",
                                LineNumber = 68,
                                FileName = "Button.cs",
                                InApp = false
                            },
                            new()
                            {
                                Function = "UnityEngine.Events.UnityEvent.Invoke ()",
                                AbsolutePath = "/Users/bokken/buildslave/unity/build/Runtime/Export/UnityEvent/UnityEvent/UnityEvent_0.cs",
                                LineNumber = 58,
                                FileName = "UnityEvent_0.cs",
                                InApp = false
                            },
                            new()
                            {
                                Function = "UnityEngine.Events.InvokableCall.Invoke ()",
                                AbsolutePath = "/Users/bokken/buildslave/unity/build/Runtime/Export/UnityEvent/UnityEvent.cs",
                                LineNumber = 166,
                                FileName = "UnityEvent.cs",
                                InApp = false
                            },
                            new()
                            {
                                Function = "BugFarm.AssertFalse ()",
                                AbsolutePath = "Assets/Scripts/BugFarm.cs",
                                LineNumber = 28,
                                FileName = "BugFarm.cs",
                                InApp = true
                            },
                            new()
                            {
                                Function = "UnityEngine.Assertions.Assert.AreEqual[T] (T expected, T actual)",
                                AbsolutePath = "/Users/bokken/buildslave/unity/build/Runtime/Export/Assertions/Assert/AssertGeneric.cs",
                                LineNumber = 13,
                                FileName = "AssertGeneric.cs",
                                InApp = false
                            },
                            new()
                            {
                                Function = "UnityEngine.Assertions.Assert.AreEqual[T] (T expected, T actual, System.String message)",
                                AbsolutePath = "/Users/bokken/buildslave/unity/build/Runtime/Export/Assertions/Assert/AssertGeneric.cs",
                                LineNumber = 19,
                                FileName = "AssertGeneric.cs",
                                InApp = false
                            },
                            new()
                            {
                                Function = "UnityEngine.Assertions.Assert.AreEqual[T] (T expected, T actual, System.String message, System.Collections.Generic.IEqualityComparer`1[T] comparer)",
                                AbsolutePath = "/Users/bokken/buildslave/unity/build/Runtime/Export/Assertions/Assert/AssertGeneric.cs",
                                LineNumber = 31,
                                FileName = "AssertGeneric.cs",
                                InApp = false
                            },
                            new()
                            {
                                Function = "UnityEngine.Assertions.Assert.Fail (System.String message, System.String userMessage)",
                                AbsolutePath = "/Users/bokken/buildslave/unity/build/Runtime/Export/Assertions/Assert/AssertBase.cs",
                                LineNumber = 29,
                                FileName = "AssertBase.cs",
                                InApp = false
                            },
                        }
                    }
                } },
            // TODO: Current parsing logic isn't able to parse this (editor only stack trace)
//             new object[] {
//                 "🕷️🕷️🕷️ Spider message 🕷️🕷️🕷️🕷️",
//                 @"#0 GetStacktrace(int)
// #1 DebugStringToFile(DebugStringToFileData const&)
// #2 DebugLogHandler_CUSTOM_Internal_Log(LogType, LogOption, ScriptingBackendNativeStringPtrOpaque*, ScriptingBackendNativeObjectPtrOpaque*)
// #3 (Mono JIT Code) (wrapper managed-to-native) UnityEngine.DebugLogHandler:Internal_Log (UnityEngine.LogType,UnityEngine.LogOption,string,UnityEngine.Object)",
//                 new SentryException
//                 {
//                     Value = "🕷️🕷️🕷️ Spider message 🕷️🕷️🕷️🕷️",
//                     Stacktrace = new SentryStackTrace
//                     {
//                         Frames = new List<SentryStackFrame>
//                         {
//                             new()
//                             {
//                                 Function = "UnityEngine.DebugLogHandler:Internal_Log (UnityEngine.LogType,UnityEngine.LogOption,string,UnityEngine.Object)",
//                                 InApp = false
//                             },
//                             new()
//                             {
//                                 Function = "DebugLogHandler_CUSTOM_Internal_Log(LogType, LogOption, ScriptingBackendNativeStringPtrOpaque*, ScriptingBackendNativeObjectPtrOpaque*)",
//                             },
//                             new()
//                             {
//                                 Function = "DebugStringToFile(DebugStringToFileData const&)",
//                                 InApp = false
//                             },
//                             new()
//                             {
//                                 Function = "GetStacktrace(int)",
//                             },
//                         }
//                     }
//                 } },
        };

        private static void AssertEqual(SentryException expected, SentryException actual)
        {
            Assert.AreEqual(expected.Value, actual.Value);
            Assert.AreEqual(expected.ThreadId, actual.ThreadId);
            Assert.AreEqual(expected.Module, actual.Module);
            Assert.AreEqual(expected.Type, actual.Type);
            if (expected.Stacktrace is not null)
            {
                if (expected.Stacktrace.Frames is not null)
                {
                    Assert.AreEqual(expected.Stacktrace.Frames.Count, actual.Stacktrace!.Frames!.Count);
                    for (var i = 0; i < expected.Stacktrace.Frames.Count; i++)
                    {
                        Assert.AreEqual(expected.Stacktrace.Frames[i].Function, actual.Stacktrace.Frames[i].Function);
                        Assert.AreEqual(expected.Stacktrace.Frames[i].Module, actual.Stacktrace.Frames[i].Module);
                        Assert.AreEqual(expected.Stacktrace.Frames[i].Package, actual.Stacktrace.Frames[i].Package);
                        Assert.AreEqual(expected.Stacktrace.Frames[i].Platform, actual.Stacktrace.Frames[i].Platform);
                        Assert.AreEqual(expected.Stacktrace.Frames[i].AbsolutePath, actual.Stacktrace.Frames[i].AbsolutePath);
                        Assert.AreEqual(expected.Stacktrace.Frames[i].ColumnNumber, actual.Stacktrace.Frames[i].ColumnNumber);
                        Assert.AreEqual(expected.Stacktrace.Frames[i].FileName, actual.Stacktrace.Frames[i].FileName);
                        Assert.AreEqual(expected.Stacktrace.Frames[i].ImageAddress, actual.Stacktrace.Frames[i].ImageAddress);
                        Assert.AreEqual(expected.Stacktrace.Frames[i].InApp, actual.Stacktrace.Frames[i].InApp);
                        Assert.AreEqual(expected.Stacktrace.Frames[i].InstructionAddress, actual.Stacktrace.Frames[i].InstructionAddress);
                        Assert.AreEqual(expected.Stacktrace.Frames[i].InstructionOffset, actual.Stacktrace.Frames[i].InstructionOffset);
                        Assert.AreEqual(expected.Stacktrace.Frames[i].LineNumber, actual.Stacktrace.Frames[i].LineNumber);
                        Assert.AreEqual(expected.Stacktrace.Frames[i].PostContext, actual.Stacktrace.Frames[i].PostContext);
                        Assert.AreEqual(expected.Stacktrace.Frames[i].PreContext, actual.Stacktrace.Frames[i].PreContext);
                        Assert.AreEqual(expected.Stacktrace.Frames[i].SymbolAddress, actual.Stacktrace.Frames[i].SymbolAddress);
                    }
                }
                else
                {
                    Assert.Null(actual.Stacktrace!.Frames);
                }
            }
            else
            {
                Assert.Null(actual.Stacktrace);
            }
            if (expected.Mechanism is not null)
            {
                Assert.AreEqual(expected.Mechanism.Description, actual.Mechanism!.Description);
                Assert.AreEqual(expected.Mechanism.Handled, actual.Mechanism.Handled);
                Assert.AreEqual(expected.Mechanism.Type, actual.Mechanism.Type);
                Assert.AreEqual(expected.Mechanism.HelpLink, actual.Mechanism.HelpLink);
                Assert.AreEqual(expected.Mechanism.Data, actual.Mechanism.Data);
                if (expected.Mechanism.Data is not null)
                {
                    Assert.True(expected.Mechanism.Data.Keys.SequenceEqual(actual.Mechanism.Data.Keys));
                    Assert.True(expected.Mechanism.Data.Values.SequenceEqual(actual.Mechanism.Data.Values));
                }
                else
                {
                    Assert.Null(actual.Mechanism.Meta);
                }
                if (expected.Mechanism.Meta is not null)
                {
                    Assert.True(expected.Mechanism.Meta.Keys.SequenceEqual(actual.Mechanism.Meta.Keys));
                    Assert.True(expected.Mechanism.Meta.Values.SequenceEqual(actual.Mechanism.Meta.Values));
                }
                else
                {
                    Assert.Null(actual.Mechanism.Meta);
                }
            }
            else
            {
                Assert.Null(actual.Mechanism);
            }
        }
    }
}


<|start_filename|>src/Sentry.Unity.Android/AndroidJavaScopeObserver.cs<|end_filename|>
using Sentry.Extensibility;
using Sentry.Unity.Json;
using UnityEngine;

namespace Sentry.Unity.Android
{
    /// <summary>
    /// Scope Observer for Android through Java (JNI).
    /// </summary>
    /// <see href="https://github.com/getsentry/sentry-java"/>
    public class AndroidJavaScopeObserver : IScopeObserver
    {
        private readonly SentryOptions _options;

        public AndroidJavaScopeObserver(SentryOptions options) => _options = options;

        private AndroidJavaObject GetSentryJava() => new AndroidJavaClass("io.sentry.Sentry");

        public void AddBreadcrumb(Breadcrumb breadcrumb)
        {
            AndroidJNI.AttachCurrentThread();

            _options.DiagnosticLogger?.LogDebug("Android Scope Sync - Adding breadcrumb m:\"{0}\" l:\"{1}\"",
                breadcrumb.Message,
                breadcrumb.Level);

            using var sentry = GetSentryJava();
            using var javaBreadcrumb = new AndroidJavaObject("io.sentry.Breadcrumb");
            javaBreadcrumb.Set("message", breadcrumb.Message);
            javaBreadcrumb.Set("type", breadcrumb.Type);
            javaBreadcrumb.Set("category", breadcrumb.Category);
            using var javaLevel = breadcrumb.Level.ToJavaSentryLevel();
            javaBreadcrumb.Set("level", javaLevel);
            sentry.CallStatic("addBreadcrumb", javaBreadcrumb, null);
        }

        public void SetExtra(string key, object? value)
        {
            AndroidJNI.AttachCurrentThread();

            _options.DiagnosticLogger?.LogDebug("Android Scope Sync - Setting Extra k:\"{0}\" v:\"{1}\"", key, value);

            string? extraValue = null;
            if (value is not null)
            {
                extraValue = SafeSerializer.SerializeSafely(value);
                if (extraValue is null)
                {
                    return;
                }
            }

            using var sentry = GetSentryJava();
            sentry.CallStatic("setExtra", key, extraValue);
        }

        public void SetTag(string key, string value)
        {
            AndroidJNI.AttachCurrentThread();

            _options.DiagnosticLogger?.LogDebug("Android Scope Sync - Setting Tag k:\"{0}\" v:\"{1}\"", key, value);

            using var sentry = GetSentryJava();
            sentry.CallStatic("setTag", key, value);
        }

        public void UnsetTag(string key)
        {
            AndroidJNI.AttachCurrentThread();

            _options.DiagnosticLogger?.LogDebug("Android Scope Sync - Unsetting Tag k:\"{0}\"", key);

            using var sentry = GetSentryJava();
            sentry.CallStatic("removeTag", key);
        }

        public void SetUser(User? user)
        {
            AndroidJNI.AttachCurrentThread();

            AndroidJavaObject? javaUser = null;
            try
            {
                if (user is not null)
                {
                    _options.DiagnosticLogger?.LogDebug("Android Scope Sync - Setting User i:\"{0}\" n:\"{1}\"",
                        user.Id,
                        user.Username);

                    javaUser = new AndroidJavaObject("io.sentry.protocol.User");
                    javaUser.Set("email", user.Email);
                    javaUser.Set("id", user.Id);
                    javaUser.Set("username", user.Username);
                    javaUser.Set("ipAddress", user.IpAddress);
                }
                else
                {
                    _options.DiagnosticLogger?.LogDebug("Android Scope Sync - Unsetting User");
                }
                using var sentry = GetSentryJava();
                sentry.CallStatic("setUser", javaUser);
            }
            finally
            {
                javaUser?.Dispose();
            }
        }
    }
}
