diff --git a/CHANGELOG.md b/CHANGELOG.md index 775f3248c9..6efca2b2dd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ ### Features +- Native iOS events are now exposed to the dotnet layer for users to hook through SentryOptions.BeforeSend and SentryOptions.OnCrashedLastRun ([#2102](https://github.com/getsentry/sentry-dotnet/pull/3958)) - Users can now register their own MAUI controls for breadcrumb creation ([#3997](https://github.com/getsentry/sentry-dotnet/pull/3997)) - Serilog scope properties are now sent with Sentry events ([#3976](https://github.com/getsentry/sentry-dotnet/pull/3976)) - The sample seed used for sampling decisions is now propagated, for use in downstream custom trace samplers ([#3951](https://github.com/getsentry/sentry-dotnet/pull/3951)) diff --git a/modules/Ben.Demystifier b/modules/Ben.Demystifier index fe04751006..0a6b30f6ac 160000 --- a/modules/Ben.Demystifier +++ b/modules/Ben.Demystifier @@ -1 +1 @@ -Subproject commit fe0475100614508fff1c76aa896e17807adde83f +Subproject commit 0a6b30f6ac6892ff45b9dcd5b07d710e5228256e diff --git a/modules/sentry-native b/modules/sentry-native index 2a3bdfb9e9..ccef7125b3 160000 --- a/modules/sentry-native +++ b/modules/sentry-native @@ -1 +1 @@ -Subproject commit 2a3bdfb9e9899574ee2558df2eea39dfa097f103 +Subproject commit ccef7125b3783210f44ee6b100df7278e6ba3eff diff --git a/samples/Sentry.Samples.Ios/AppDelegate.cs b/samples/Sentry.Samples.Ios/AppDelegate.cs index 80e9ea09a5..2d50a320c3 100644 --- a/samples/Sentry.Samples.Ios/AppDelegate.cs +++ b/samples/Sentry.Samples.Ios/AppDelegate.cs @@ -28,6 +28,17 @@ public override bool FinishedLaunching(UIApplication application, NSDictionary l options.Native.EnableAppHangTracking = true; options.CacheDirectoryPath = Path.GetTempPath(); + + options.SetBeforeSend((evt, _) => + { + evt.SetTag("dotnet-iOS-Native-Before", "Hello World"); + return evt; + }); + + options.OnCrashedLastRun = e => + { + Console.WriteLine(e); + }; }); // create a new window instance based on the screen size diff --git a/samples/Sentry.Samples.Ios/Sentry.Samples.Ios.csproj b/samples/Sentry.Samples.Ios/Sentry.Samples.Ios.csproj index 76b122d042..b1820343e6 100644 --- a/samples/Sentry.Samples.Ios/Sentry.Samples.Ios.csproj +++ b/samples/Sentry.Samples.Ios/Sentry.Samples.Ios.csproj @@ -1,7 +1,7 @@ - net9.0-ios18.0 + net9.0-ios Exe enable true diff --git a/src/Sentry.Bindings.Cocoa/ApiDefinitions.cs b/src/Sentry.Bindings.Cocoa/ApiDefinitions.cs index 3bbfc298dc..77ad1d5797 100644 --- a/src/Sentry.Bindings.Cocoa/ApiDefinitions.cs +++ b/src/Sentry.Bindings.Cocoa/ApiDefinitions.cs @@ -27,7 +27,6 @@ namespace Sentry.CocoaSdk; // typedef SentryEvent * _Nullable (^SentryBeforeSendEventCallback)(SentryEvent * _Nonnull); [Internal] -[return: NullAllowed] delegate SentryEvent SentryBeforeSendEventCallback (SentryEvent @event); // typedef id _Nullable (^SentryBeforeSendSpanCallback)(id _Nonnull); diff --git a/src/Sentry/Internal/Extensions/JsonExtensions.cs b/src/Sentry/Internal/Extensions/JsonExtensions.cs index fa29909eac..8bc25eba83 100644 --- a/src/Sentry/Internal/Extensions/JsonExtensions.cs +++ b/src/Sentry/Internal/Extensions/JsonExtensions.cs @@ -213,6 +213,31 @@ public static void Deconstruct(this JsonProperty jsonProperty, out string name, return double.Parse(json.ToString()!, CultureInfo.InvariantCulture); } + /// + /// Safety value to deal with native serialization - allows datetimeoffset to come in as a long or string value + /// + /// + /// + /// + public static DateTimeOffset? GetSafeDateTimeOffset(this JsonElement json, string propertyName) + { + DateTimeOffset? result = null; + var dtRaw = json.GetPropertyOrNull(propertyName); + if (dtRaw != null) + { + if (dtRaw.Value.ValueKind == JsonValueKind.Number) + { + var epoch = Convert.ToInt64(dtRaw.Value.GetDouble()); + result = DateTimeOffset.FromUnixTimeSeconds(epoch); + } + else + { + result = dtRaw.Value.GetDateTimeOffset(); + } + } + return result; + } + public static long? GetHexAsLong(this JsonElement json) { // If the address is in json as a number, we can just use it. diff --git a/src/Sentry/Platforms/Cocoa/Extensions/CocoaExtensions.cs b/src/Sentry/Platforms/Cocoa/Extensions/CocoaExtensions.cs index e0fb5e7bc0..cf6776a450 100644 --- a/src/Sentry/Platforms/Cocoa/Extensions/CocoaExtensions.cs +++ b/src/Sentry/Platforms/Cocoa/Extensions/CocoaExtensions.cs @@ -169,6 +169,24 @@ public static NSDictionary ToNSDictionary( d.Keys.ToArray()); } + public static NSDictionary ToNSDictionaryStrings( + this IEnumerable> dict) + { + var d = new Dictionary(); + foreach (var item in dict) + { + if (item.Value != null) + { + d.Add((NSString)item.Key, new NSString(item.Value)); + } + } + + return NSDictionary + .FromObjectsAndKeys( + d.Values.ToArray(), + d.Keys.ToArray()); + } + public static NSDictionary? ToNullableNSDictionary( this ICollection> dict) => dict.Count == 0 ? null : dict.ToNSDictionary(); @@ -224,4 +242,126 @@ public static object ToObject(this NSNumber n) $"NSNumber \"{n.StringValue}\" has an unknown ObjCType \"{n.ObjCType}\" (Class: \"{n.Class.Name}\")") }; } + + public static void CopyToCocoaSentryEvent(this SentryEvent managed, CocoaSdk.SentryEvent native) + { + // we only support a subset of mutated data to be passed back to the native SDK at this time + native.ServerName = managed.ServerName; + native.Dist = managed.Distribution; + native.Logger = managed.Logger; + native.ReleaseName = managed.Release; + native.Environment = managed.Environment; + native.Transaction = managed.TransactionName!; + native.Message = managed.Message?.ToCocoaSentryMessage(); + native.Tags = managed.Tags?.ToNSDictionaryStrings(); + native.Extra = managed.Extra?.ToNSDictionary(); + native.Breadcrumbs = managed.Breadcrumbs?.Select(x => x.ToCocoaBreadcrumb()).ToArray(); + native.User = managed.User?.ToCocoaUser(); + + if (managed.Level != null) + { + native.Level = managed.Level.Value.ToCocoaSentryLevel(); + } + + if (managed.Exception != null) + { + native.Error = new NSError(new NSString(managed.Exception.ToString()), IntPtr.Zero); + } + } + + public static SentryEvent? ToSentryEvent(this CocoaSdk.SentryEvent sentryEvent) + { + using var stream = sentryEvent.ToJsonStream(); + if (stream == null) + return null; + + using var json = JsonDocument.Parse(stream); + var exception = sentryEvent.Error == null ? null : new NSErrorException(sentryEvent.Error); + var ev = SentryEvent.FromJson(json.RootElement, exception); + return ev; + } + + public static CocoaSdk.SentryMessage ToCocoaSentryMessage(this SentryMessage msg) + { + var native = new CocoaSdk.SentryMessage(msg.Formatted ?? string.Empty); + native.Params = msg.Params?.Select(x => x.ToString()!).ToArray() ?? new string[0]; + + return native; + } + + // not tested or needed yet - leaving for future just in case + // public static CocoaSdk.SentryThread ToCocoaSentryThread(this SentryThread thread) + // { + // var id = NSNumber.FromInt32(thread.Id ?? 0); + // var native = new CocoaSdk.SentryThread(id); + // native.Crashed = thread.Crashed; + // native.Current = thread.Current; + // native.Name = thread.Name; + // native.Stacktrace = thread.Stacktrace?.ToCocoaSentryStackTrace(); + // // native.IsMain = not in dotnet + // return native; + // } + // + // public static CocoaSdk.SentryRequest ToCocoaSentryRequest(this SentryRequest request) + // { + // var native = new CocoaSdk.SentryRequest(); + // native.Cookies = request.Cookies; + // native.Headers = request.Headers?.ToNSDictionaryStrings(); + // native.Method = request.Method; + // native.QueryString = request.QueryString; + // native.Url = request.Url; + // + // // native.BodySize does not exist in dotnet + // return native; + // } + // + + // public static CocoaSdk.SentryException ToCocoaSentryException(this SentryException ex) + // { + // var native = new CocoaSdk.SentryException(ex.Value ?? string.Empty, ex.Type ?? string.Empty); + // native.Module = ex.Module; + // native.Mechanism = ex.Mechanism?.ToCocoaSentryMechanism(); + // native.Stacktrace = ex.Stacktrace?.ToCocoaSentryStackTrace(); + // // not part of native - ex.ThreadId; + // return native; + // } + // + // public static CocoaSdk.SentryStacktrace ToCocoaSentryStackTrace(this SentryStackTrace stackTrace) + // { + // var frames = stackTrace.Frames?.Select(x => x.ToCocoaSentryFrame()).ToArray() ?? new CocoaSdk.SentryFrame[0]; + // var native = new CocoaSdk.SentryStacktrace(frames, new NSDictionary()); + // // native.Register & native.Snapshot missing in dotnet + // return native; + // } + // + // public static CocoaSdk.SentryFrame ToCocoaSentryFrame(this SentryStackFrame frame) + // { + // var native = new CocoaSdk.SentryFrame(); + // native.Module = frame.Module; + // native.Package = frame.Package; + // native.InstructionAddress = frame.InstructionAddress?.ToString(); + // native.Function = frame.Function; + // native.Platform = frame.Platform; + // native.ColumnNumber = frame.ColumnNumber; + // native.FileName = frame.FileName; + // native.InApp = frame.InApp; + // native.ImageAddress = frame.ImageAddress?.ToString(); + // native.LineNumber = frame.LineNumber; + // native.SymbolAddress = frame.SymbolAddress?.ToString(); + // + // // native.StackStart = doesn't exist in dotnet + // return native; + // } + // + // public static CocoaSdk.SentryMechanism ToCocoaSentryMechanism(this Mechanism mechanism) + // { + // var native = new CocoaSdk.SentryMechanism(mechanism.Type); + // native.Synthetic = mechanism.Synthetic; + // native.Handled = mechanism.Handled; + // native.Desc = mechanism.Description; + // native.HelpLink = mechanism.HelpLink; + // native.Data = mechanism.Data?.ToNSDictionary(); + // // TODO: Meta does not currently translate in dotnet - native.Meta = null; + // return native; + // } } diff --git a/src/Sentry/Platforms/Cocoa/Extensions/EnumExtensions.cs b/src/Sentry/Platforms/Cocoa/Extensions/EnumExtensions.cs index 3cb7df8c99..feb2a37e91 100644 --- a/src/Sentry/Platforms/Cocoa/Extensions/EnumExtensions.cs +++ b/src/Sentry/Platforms/Cocoa/Extensions/EnumExtensions.cs @@ -4,7 +4,16 @@ internal static class EnumExtensions { // These align, so we can just cast public static SentryLevel ToSentryLevel(this CocoaSdk.SentryLevel level) => (SentryLevel)level; - public static CocoaSdk.SentryLevel ToCocoaSentryLevel(this SentryLevel level) => (CocoaSdk.SentryLevel)level; + + public static CocoaSdk.SentryLevel ToCocoaSentryLevel(this SentryLevel level) => level switch + { + SentryLevel.Debug => CocoaSdk.SentryLevel.Debug, + SentryLevel.Info => CocoaSdk.SentryLevel.Info, + SentryLevel.Warning => CocoaSdk.SentryLevel.Warning, + SentryLevel.Error => CocoaSdk.SentryLevel.Error, + SentryLevel.Fatal => CocoaSdk.SentryLevel.Fatal, + _ => throw new ArgumentOutOfRangeException(nameof(level), level, null) + }; public static BreadcrumbLevel ToBreadcrumbLevel(this CocoaSdk.SentryLevel level) => level switch diff --git a/src/Sentry/Platforms/Cocoa/Extensions/SentryEventExtensions.cs b/src/Sentry/Platforms/Cocoa/Extensions/SentryEventExtensions.cs deleted file mode 100644 index ebb7c735ba..0000000000 --- a/src/Sentry/Platforms/Cocoa/Extensions/SentryEventExtensions.cs +++ /dev/null @@ -1,45 +0,0 @@ -// using Sentry.Extensibility; -// using Sentry.Protocol.Envelopes; -// -// namespace Sentry.Cocoa.Extensions; -// -// internal static class SentryEventExtensions -// { -// /* -// * These methods map between a SentryEvent and it's Cocoa counterpart by serializing as JSON into memory on one side, -// * then deserializing back to an object on the other side. It is not expected to be performant, as this code is only -// * used when a BeforeSend option is set, and then only when an event is captured by the Cocoa SDK (which should be -// * relatively rare). -// * -// * This approach avoids having to write to/from methods for the entire object graph. However, it's also important to -// * recognize that there's not necessarily a one-to-one mapping available on all objects (even through serialization) -// * between the two SDKs, so some optional details may be lost when roundtripping. That's generally OK, as this is -// * still better than nothing. If a specific part of the object graph becomes important to roundtrip, we can consider -// * updating the objects on either side. -// */ -// -// public static SentryEvent ToSentryEvent(this CocoaSdk.SentryEvent sentryEvent, SentryCocoaSdkOptions nativeOptions) -// { -// using var stream = sentryEvent.ToJsonStream()!; -// //stream.Seek(0, SeekOrigin.Begin); ?? -// -// using var json = JsonDocument.Parse(stream); -// var exception = sentryEvent.Error == null ? null : new NSErrorException(sentryEvent.Error); -// return SentryEvent.FromJson(json.RootElement, exception); -// } -// -// public static CocoaSdk.SentryEvent ToCocoaSentryEvent(this SentryEvent sentryEvent, SentryOptions options, SentryCocoaSdkOptions nativeOptions) -// { -// var envelope = Envelope.FromEvent(sentryEvent); -// -// using var stream = new MemoryStream(); -// envelope.Serialize(stream, options.DiagnosticLogger); -// stream.Seek(0, SeekOrigin.Begin); -// -// using var data = NSData.FromStream(stream)!; -// var cocoaEnvelope = CocoaSdk.PrivateSentrySDKOnly.EnvelopeWithData(data); -// -// var cocoaEvent = (CocoaSdk.SentryEvent) cocoaEnvelope.Items[0]; -// return cocoaEvent; -// } -// } diff --git a/src/Sentry/Platforms/Cocoa/SentryOptions.cs b/src/Sentry/Platforms/Cocoa/SentryOptions.cs index 6b0e9587e6..91172d9938 100644 --- a/src/Sentry/Platforms/Cocoa/SentryOptions.cs +++ b/src/Sentry/Platforms/Cocoa/SentryOptions.cs @@ -197,7 +197,6 @@ internal NativeOptions(SentryOptions options) /// public NSUrlSessionDelegate? UrlSessionDelegate { get; set; } = null; - // ---------- Other ---------- /// diff --git a/src/Sentry/Platforms/Cocoa/SentrySdk.cs b/src/Sentry/Platforms/Cocoa/SentrySdk.cs index dcc8e48006..8908cd78c0 100644 --- a/src/Sentry/Platforms/Cocoa/SentrySdk.cs +++ b/src/Sentry/Platforms/Cocoa/SentrySdk.cs @@ -86,29 +86,90 @@ private static void InitSentryCocoaSdk(SentryOptions options) } } - // TODO: Finish SentryEventExtensions to enable these - - // if (options.Native.EnableBeforeSend && options.BeforeSend is { } beforeSend) - // { - // nativeOptions.BeforeSend = evt => - // { - // var sentryEvent = evt.ToSentryEvent(nativeOptions); - // var result = beforeSend(sentryEvent)?.ToCocoaSentryEvent(options, nativeOptions); - // - // // Note: Nullable result is allowed but delegate is generated incorrectly - // // See https://github.com/xamarin/xamarin-macios/issues/15299#issuecomment-1201863294 - // return result!; - // }; - // } - - // if (options.Native.OnCrashedLastRun is { } onCrashedLastRun) - // { - // nativeOptions.OnCrashedLastRun = evt => - // { - // var sentryEvent = evt.ToSentryEvent(nativeOptions); - // onCrashedLastRun(sentryEvent); - // }; - // } + nativeOptions.BeforeSend = evt => + { + // When we have an unhandled managed exception, we send that to Sentry twice - once managed and once native. + // The managed exception is what a .NET developer would expect, and it is sent by the Sentry.NET SDK + // But we also get a native SIGABRT since it crashed the application, which is sent by the Sentry Cocoa SDK. + + // There should only be one exception on the event in this case + if (evt.Exceptions?.Length == 1) + { + // It will match the following characteristics + var ex = evt.Exceptions[0]; + + // Thankfully, sometimes we can see Xamarin's unhandled exception handler on the stack trace, so we can filter + // them out. Here is the function that calls abort(), which we will use as a filter: + // https://github.com/xamarin/xamarin-macios/blob/c55fbdfef95028ba03d0f7a35aebca03bd76f852/runtime/runtime.m#L1114-L1122 + if (ex.Type == "SIGABRT" && ex.Value == "Signal 6, Code 0" && + ex.Stacktrace?.Frames.Any(f => f.Function == "xamarin_unhandled_exception_handler") is true) + { + // Don't send it + options.LogDebug("Discarded {0} error ({1}). Captured as managed exception instead.", ex.Type, + ex.Value); + return null!; + } + + // Similar workaround for NullReferenceExceptions. We don't have any easy way to know whether the + // exception is managed code (compiled to native) or original native code though. + // See: https://github.com/getsentry/sentry-dotnet/issues/3776 + if (ex.Type == "EXC_BAD_ACCESS") + { + // Don't send it + options.LogDebug("Discarded {0} error ({1}). Captured as managed exception instead.", ex.Type, + ex.Value); + return null!; + } + } + + // we run our SIGABRT checks first before handing over to user events + // because we delegate to user code, we need to protect anything that could happen in this event + if (options.BeforeSendInternal == null) + return evt; + + try + { + var sentryEvent = evt.ToSentryEvent(); + if (sentryEvent == null) + return evt; + + var result = options.BeforeSendInternal(sentryEvent, null!); + if (result == null) + return null!; + + // we only support a subset of mutated data to be passed back to the native SDK at this time + result.CopyToCocoaSentryEvent(evt); + + // Note: Nullable result is allowed but delegate is generated incorrectly + // See https://github.com/xamarin/xamarin-macios/issues/15299#issuecomment-1201863294 + return evt!; + } + catch (Exception ex) + { + options.LogError(ex, "Before Send Error"); + return evt; + } + }; + + if (options.OnCrashedLastRun is { } onCrashedLastRun) + { + nativeOptions.OnCrashedLastRun = evt => + { + // because we delegate to user code, we need to protect anything that could happen in this event + try + { + var sentryEvent = evt.ToSentryEvent(); + if (sentryEvent != null) + { + onCrashedLastRun(sentryEvent); + } + } + catch (Exception ex) + { + options.LogError(ex, "Crashed Last Run Error"); + } + }; + } // These options are from Cocoa's SentryOptions nativeOptions.AttachScreenshot = options.Native.AttachScreenshot; @@ -145,43 +206,6 @@ private static void InitSentryCocoaSdk(SentryOptions options) // nativeOptions.DefaultIntegrations // nativeOptions.EnableProfiling (deprecated) - // When we have an unhandled managed exception, we send that to Sentry twice - once managed and once native. - // The managed exception is what a .NET developer would expect, and it is sent by the Sentry.NET SDK - // But we also get a native SIGABRT since it crashed the application, which is sent by the Sentry Cocoa SDK. - nativeOptions.BeforeSend = evt => - { - // There should only be one exception on the event in this case - if (evt.Exceptions?.Length == 1) - { - // It will match the following characteristics - var ex = evt.Exceptions[0]; - - // Thankfully, sometimes we can see Xamarin's unhandled exception handler on the stack trace, so we can filter - // them out. Here is the function that calls abort(), which we will use as a filter: - // https://github.com/xamarin/xamarin-macios/blob/c55fbdfef95028ba03d0f7a35aebca03bd76f852/runtime/runtime.m#L1114-L1122 - if (ex.Type == "SIGABRT" && ex.Value == "Signal 6, Code 0" && - ex.Stacktrace?.Frames.Any(f => f.Function == "xamarin_unhandled_exception_handler") is true) - { - // Don't send it - options.LogDebug("Discarded {0} error ({1}). Captured as managed exception instead.", ex.Type, ex.Value); - return null!; - } - - // Similar workaround for NullReferenceExceptions. We don't have any easy way to know whether the - // exception is managed code (compiled to native) or original native code though. - // See: https://github.com/getsentry/sentry-dotnet/issues/3776 - if (ex.Type == "EXC_BAD_ACCESS") - { - // Don't send it - options.LogDebug("Discarded {0} error ({1}). Captured as managed exception instead.", ex.Type, ex.Value); - return null!; - } - } - - // Other event, send as normal - return evt; - }; - // Set hybrid SDK name SentryCocoaHybridSdk.SetSdkName("sentry.cocoa.dotnet"); diff --git a/src/Sentry/SentryEvent.cs b/src/Sentry/SentryEvent.cs index b580a89881..81abbe4ddd 100644 --- a/src/Sentry/SentryEvent.cs +++ b/src/Sentry/SentryEvent.cs @@ -286,11 +286,29 @@ public void WriteTo(Utf8JsonWriter writer, IDiagnosticLogger? logger) /// public static SentryEvent FromJson(JsonElement json) => FromJson(json, null); + private static SentryLevel? SafeLevelFromJson(JsonElement json) + { + var levelString = json.GetPropertyOrNull("level")?.GetString(); + if (levelString == null) + return null; + + // Native SentryLevel.None does not exist in dotnet + return levelString.ToLowerInvariant() switch + { + "debug" => SentryLevel.Debug, + "info" => SentryLevel.Info, + "warning" => SentryLevel.Warning, + "fatal" => SentryLevel.Fatal, + "error" => SentryLevel.Error, + _ => null + }; + } + internal static SentryEvent FromJson(JsonElement json, Exception? exception) { var modules = json.GetPropertyOrNull("modules")?.GetStringDictionaryOrNull(); var eventId = json.GetPropertyOrNull("event_id")?.Pipe(SentryId.FromJson) ?? SentryId.Empty; - var timestamp = json.GetPropertyOrNull("timestamp")?.GetDateTimeOffset(); + var timestamp = json.GetSafeDateTimeOffset("timestamp"); // Native sentryevents are serialized to epoch timestamps var message = json.GetPropertyOrNull("logentry")?.Pipe(SentryMessage.FromJson); var logger = json.GetPropertyOrNull("logger")?.GetString(); var platform = json.GetPropertyOrNull("platform")?.GetString(); @@ -299,7 +317,7 @@ internal static SentryEvent FromJson(JsonElement json, Exception? exception) var distribution = json.GetPropertyOrNull("dist")?.GetString(); var exceptionValues = json.GetPropertyOrNull("exception")?.GetPropertyOrNull("values")?.EnumerateArray().Select(SentryException.FromJson).ToList().Pipe(v => new SentryValues(v)); var threadValues = json.GetPropertyOrNull("threads")?.GetPropertyOrNull("values")?.EnumerateArray().Select(SentryThread.FromJson).ToList().Pipe(v => new SentryValues(v)); - var level = json.GetPropertyOrNull("level")?.GetString()?.ParseEnum(); + var transaction = json.GetPropertyOrNull("transaction")?.GetString(); var request = json.GetPropertyOrNull("request")?.Pipe(SentryRequest.FromJson); var contexts = json.GetPropertyOrNull("contexts")?.Pipe(SentryContexts.FromJson); @@ -310,7 +328,7 @@ internal static SentryEvent FromJson(JsonElement json, Exception? exception) var breadcrumbs = json.GetPropertyOrNull("breadcrumbs")?.EnumerateArray().Select(Breadcrumb.FromJson).ToList(); var extra = json.GetPropertyOrNull("extra")?.GetDictionaryOrNull(); var tags = json.GetPropertyOrNull("tags")?.GetStringDictionaryOrNull(); - + var level = SafeLevelFromJson(json); var debugMeta = json.GetPropertyOrNull("debug_meta")?.Pipe(DebugMeta.FromJson); return new SentryEvent(exception, timestamp, eventId) diff --git a/src/Sentry/SentryOptions.cs b/src/Sentry/SentryOptions.cs index 7c99988acf..69a87685a7 100644 --- a/src/Sentry/SentryOptions.cs +++ b/src/Sentry/SentryOptions.cs @@ -1075,6 +1075,14 @@ public StackTraceMode StackTraceMode /// public Func? CrashedLastRun { get; set; } +#if IOS || MACCATALYST + // this event currently isn't being pushed from Android + /// + /// Delegate which is run with event information if the application crashed during last run. + /// + public Action? OnCrashedLastRun { get; set; } +#endif + /// /// /// Gets the used to create spans. diff --git a/test/Sentry.Tests/Platforms/iOS/CocoaExtensionsTests.cs b/test/Sentry.Tests/Platforms/iOS/CocoaExtensionsTests.cs new file mode 100644 index 0000000000..04e782e639 --- /dev/null +++ b/test/Sentry.Tests/Platforms/iOS/CocoaExtensionsTests.cs @@ -0,0 +1,118 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using FluentAssertions; +using Foundation; +using Sentry.Cocoa.Extensions; +using Xunit; + +namespace Sentry.Tests.Platforms.iOS; + +public class CocoaExtensionsTests +{ + [Fact] + public void CopyToCocoaSentryEvent_CopiesProperties() + { + var evt = new SentryEvent(new Exception("Test Exception")); + + evt.Level = SentryLevel.Debug; + evt.ServerName = "test server name"; + evt.Distribution = "test distribution"; + evt.Logger = "test logger"; + evt.Release = "test release"; + evt.Environment = "test environment"; + evt.TransactionName = "test transaction name"; + evt.Message = new SentryMessage { Params = ["Test"] }; + evt.SetTag("TestTagKey", "TestTagValue"); + evt.AddBreadcrumb(new Breadcrumb("test breadcrumb", "test type")); + evt.SetExtra("TestExtraKey", "TestExtraValue"); + evt.User = new SentryUser + { + Id = "user id", + Username = "test", + Email = "test@sentry.io", + IpAddress = "127.0.0.1" + }; + + var native = new CocoaSdk.SentryEvent(); + evt.CopyToCocoaSentryEvent(native); + + AssertEqual(evt, native); + + // message - native does not copy this over to dotnet + native.Message.Should().NotBeNull("Message should not be null"); + native.Message.Params.Should().NotBeNull("Message params should not be null"); + native.Message.Params.First().Should().Be(evt.Message!.Params!.First().ToString()); + } + + + [Fact] + public void ToSentryEvent_ConvertToManaged() + { + var native = new CocoaSdk.SentryEvent(); + + native.Timestamp = DateTimeOffset.UtcNow.ToNSDate(); + native.Level = Sentry.CocoaSdk.SentryLevel.Debug; + native.ServerName = "native server name"; + native.Dist = "native dist"; + native.Logger = "native logger"; + native.ReleaseName = "native release"; + native.Environment = "native env"; + native.Transaction = "native transaction"; + native.Message = new SentryMessage { Params = ["Test"] }.ToCocoaSentryMessage(); + native.Tags = new Dictionary { { "TestTagKey", "TestTagValue" } }.ToNSDictionaryStrings(); + native.Extra = new Dictionary { { "TestExtraKey", "TestExtraValue" } }.ToNSDictionary(); + native.Error = new NSError(new NSString("Test Error"), IntPtr.Zero); + native.Breadcrumbs = + [ + new CocoaSdk.SentryBreadcrumb(CocoaSdk.SentryLevel.Debug, "category") + ]; + native.User = new CocoaSdk.SentryUser + { + UserId = "user id", + Username = "test", + Email = "test@sentry.io", + IpAddress = "127.0.0.1" + }; + var managed = native.ToSentryEvent(); + AssertEqual(managed, native); + } + + private static void AssertEqual(SentryEvent managed, CocoaSdk.SentryEvent native) + { + native.ServerName.Should().Be(managed.ServerName, "Server Name"); + native.Dist.Should().Be(managed.Distribution, "Distribution"); + native.Logger.Should().Be(managed.Logger, "Logger"); + native.ReleaseName.Should().Be(managed.Release, "Release"); + native.Environment.Should().Be(managed.Environment, "Environment"); + native.Transaction.Should().Be(managed.TransactionName!, "Transaction"); + native.Level!.ToString().Should().Be(managed.Level.ToString(), "Level"); + + native.Extra.Should().NotBeNull("No extras found"); + native.Extra.Count.Should().Be(1, "Extras should have 1 item"); + native.Extra!.Keys![0]!.Should().Be(new NSString(managed.Extra.Keys.First()), "Extras key should match"); + native.Extra!.Values![0]!.Should().Be(NSObject.FromObject(managed.Extra.Values.First()), "Extra value should match"); + + // tags + native.Tags.Should().NotBeNull("No tags found"); + native.Tags.Count.Should().Be(1, "Tags should have 1 item"); + native.Tags!.Keys![0]!.Should().Be(new NSString(managed.Tags.Keys.First())); + native.Tags!.Values![0]!.Should().Be(new NSString(managed.Tags.Values.First())); + + // breadcrumbs + native.Breadcrumbs.Should().NotBeNull("No breadcrumbs found"); + var nb = native.Breadcrumbs!.First(); + var mb = managed.Breadcrumbs!.First(); + nb.Message.Should().Be(mb.Message, "Breadcrumb message"); + nb.Type.Should().Be(mb.Type, "Breadcrumb type"); + + // user + native.User!.UserId.Should().Be(managed.User.Id, "UserId should match"); + native.User.Email.Should().Be(managed.User.Email, "Email should match"); + native.User.Username.Should().Be(managed.User.Username, "Username should match"); + native.User.IpAddress.Should().Be(managed.User.IpAddress, "IpAddress should match"); + + // check contains because ios/android dotnet tend to move how this works from time to time + managed.Exception!.ToString().Contains(native.Error!.Domain!).Should().BeTrue("Domain message should be included in dotnet exception"); + } +}