diff --git a/CHANGELOG.md b/CHANGELOG.md index 27b9f8d2bd..f74bf20a51 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### Fixes + +- Prevent crashes from occurring on Android during OnBeforeSend ([#4022](https://github.com/getsentry/sentry-dotnet/pull/4022)) + ### Dependencies - Bump Native SDK from v0.8.0 to v0.8.1 ([#4014](https://github.com/getsentry/sentry-dotnet/pull/4014)) diff --git a/samples/Sentry.Samples.Android/MainActivity.cs b/samples/Sentry.Samples.Android/MainActivity.cs index a362b886aa..7ec70a4f9b 100644 --- a/samples/Sentry.Samples.Android/MainActivity.cs +++ b/samples/Sentry.Samples.Android/MainActivity.cs @@ -20,6 +20,17 @@ protected override void OnCreate(Bundle? savedInstanceState) // https://docs.sentry.io/platforms/android/configuration/ // Enable Native Android SDK ANR detection options.Native.AnrEnabled = true; + + options.SetBeforeSend(evt => + { + if (evt.Exception?.Message.Contains("Something you don't care want logged?") ?? false) + { + return null; // return null to filter out event + } + // or add additional data + evt.SetTag("dotnet-Android-Native-Before", "Hello World"); + return evt; + }); }); // Here's an example of adding custom scope information. diff --git a/samples/Sentry.Samples.Android/Sentry.Samples.Android.csproj b/samples/Sentry.Samples.Android/Sentry.Samples.Android.csproj index ede53769c4..ace5748bae 100644 --- a/samples/Sentry.Samples.Android/Sentry.Samples.Android.csproj +++ b/samples/Sentry.Samples.Android/Sentry.Samples.Android.csproj @@ -1,6 +1,6 @@ - net8.0-android34.0 + net8.0-android 21 Exe enable diff --git a/samples/Sentry.Samples.Ios/AppDelegate.cs b/samples/Sentry.Samples.Ios/AppDelegate.cs index 2d50a320c3..70340fdd98 100644 --- a/samples/Sentry.Samples.Ios/AppDelegate.cs +++ b/samples/Sentry.Samples.Ios/AppDelegate.cs @@ -29,8 +29,13 @@ public override bool FinishedLaunching(UIApplication application, NSDictionary l options.CacheDirectoryPath = Path.GetTempPath(); - options.SetBeforeSend((evt, _) => + options.SetBeforeSend(evt => { + if (evt.Exception?.Message.Contains("Something you don't care want logged?") ?? false) + { + return null; // return null to filter out event + } + // or add additional data evt.SetTag("dotnet-iOS-Native-Before", "Hello World"); return evt; }); diff --git a/src/Sentry/Internal/Extensions/JsonExtensions.cs b/src/Sentry/Internal/Extensions/JsonExtensions.cs index 8bc25eba83..96b28bf81b 100644 --- a/src/Sentry/Internal/Extensions/JsonExtensions.cs +++ b/src/Sentry/Internal/Extensions/JsonExtensions.cs @@ -156,7 +156,14 @@ public static void Deconstruct(this JsonProperty jsonProperty, out string name, foreach (var (name, value) in json.EnumerateObject()) { - result[name] = value.GetString(); + if (value.ValueKind == JsonValueKind.String) + { + result[name] = value.GetString(); + } + else + { + result[name] = value.ToString(); + } } return result; diff --git a/src/Sentry/Platforms/Android/Callbacks/BeforeSendCallback.cs b/src/Sentry/Platforms/Android/Callbacks/BeforeSendCallback.cs index 1fd15c6e1d..b529ec6bee 100644 --- a/src/Sentry/Platforms/Android/Callbacks/BeforeSendCallback.cs +++ b/src/Sentry/Platforms/Android/Callbacks/BeforeSendCallback.cs @@ -1,4 +1,5 @@ using Sentry.Android.Extensions; +using Sentry.Extensibility; namespace Sentry.Android.Callbacks; @@ -22,10 +23,19 @@ public BeforeSendCallback( { // Note: Hint is unused due to: // https://github.com/getsentry/sentry-dotnet/issues/1469 - - var evnt = e.ToSentryEvent(_javaOptions); - var hint = h.ToHint(); - var result = _beforeSend?.Invoke(evnt, hint); - return result?.ToJavaSentryEvent(_options, _javaOptions); + try + { + // because this can go out to user code, we want to prevent external crashing + // also, native types tend to move before dotnet does, so serialization can fail + var evnt = e.ToSentryEvent(_javaOptions); + var hint = h.ToHint(); + var result = _beforeSend?.Invoke(evnt, hint); + return result?.ToJavaSentryEvent(_options, _javaOptions); + } + catch (Exception exception) + { + _options.LogError(exception, "Before Send Error"); + return e; + } } } diff --git a/src/Sentry/Platforms/Android/Extensions/SentryEventExtensions.cs b/src/Sentry/Platforms/Android/Extensions/SentryEventExtensions.cs index 99ca913312..eb28e3de96 100644 --- a/src/Sentry/Platforms/Android/Extensions/SentryEventExtensions.cs +++ b/src/Sentry/Platforms/Android/Extensions/SentryEventExtensions.cs @@ -1,3 +1,5 @@ +using Sentry.Internal; + namespace Sentry.Android.Extensions; internal static class SentryEventExtensions @@ -17,6 +19,12 @@ internal static class SentryEventExtensions public static SentryEvent ToSentryEvent(this JavaSdk.SentryEvent sentryEvent, JavaSdk.SentryOptions javaOptions) { + if (sentryEvent.Sdk != null) + { + // when we cast this serialize this over, this value must be set + sentryEvent.Sdk.Name ??= Constants.SdkName; + sentryEvent.Sdk.Version ??= SdkVersion.Instance.Version ?? "0.0.0"; + } using var stream = new MemoryStream(); using var streamWriter = new JavaOutputStreamWriter(stream); using var jsonWriter = new JavaSdk.JsonObjectWriter(streamWriter, javaOptions.MaxDepth); @@ -31,6 +39,11 @@ public static SentryEvent ToSentryEvent(this JavaSdk.SentryEvent sentryEvent, Ja public static JavaSdk.SentryEvent ToJavaSentryEvent(this SentryEvent sentryEvent, SentryOptions options, JavaSdk.SentryOptions javaOptions) { + if (sentryEvent.Sdk != null) + { + sentryEvent.Sdk.Name ??= Constants.SdkName; + sentryEvent.Sdk.Version ??= SdkVersion.Instance.Version ?? "0.0.0"; + } using var stream = new MemoryStream(); using var jsonWriter = new Utf8JsonWriter(stream); sentryEvent.WriteTo(jsonWriter, options.DiagnosticLogger); diff --git a/src/Sentry/Platforms/Android/SentrySdk.cs b/src/Sentry/Platforms/Android/SentrySdk.cs index 3cba5dac2c..f0db0a1fef 100644 --- a/src/Sentry/Platforms/Android/SentrySdk.cs +++ b/src/Sentry/Platforms/Android/SentrySdk.cs @@ -100,7 +100,10 @@ private static void InitSentryAndroidSdk(SentryOptions options) } } - o.BeforeSend = new BeforeSendCallback(BeforeSendWrapper(options), options, o); + if (options.Android.SuppressSegfaults || (options.Native.EnableBeforeSend && options.BeforeSendInternal != null)) + { + o.BeforeSend = new BeforeSendCallback(BeforeSendWrapper(options), options, o); + } // These options are from SentryAndroidOptions o.AttachScreenshot = options.Native.AttachScreenshot; diff --git a/test/Sentry.Tests/Platforms/Android/JsonExtensionsTests.cs b/test/Sentry.Tests/Platforms/Android/JsonExtensionsTests.cs new file mode 100644 index 0000000000..8e2bd35b26 --- /dev/null +++ b/test/Sentry.Tests/Platforms/Android/JsonExtensionsTests.cs @@ -0,0 +1,126 @@ +using System; +using System.Linq; +using FluentAssertions; +using Sentry.Android.Extensions; +using Xunit; + +namespace Sentry.Tests.Platforms.Android; + +public class JsonExtensionsTests + +{ + [Fact] + public void ToJavaSentryEvent_Success() + + { + 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 = evt.ToJavaSentryEvent(new SentryOptions(), new JavaSdk.SentryOptions()); + + AssertEqual(evt, native); + } + + + [Fact] + public void ToSentryEvent_ConvertToManaged() + { + var native = new JavaSdk.SentryEvent(); + + native.Throwable = new Exception("Test Exception").ToThrowable(); + native.Timestamp = DateTimeOffset.UtcNow.ToJavaDate(); + native.Level = JavaSdk.SentryLevel.Debug; + native.ServerName = "native server name"; + native.Dist = "native dist"; + native.Logger = "native logger"; + native.Release = "native release"; + native.Environment = "native env"; + native.Transaction = "native transaction"; + native.Message = new JavaSdk.Protocol.Message + { + Params = ["Test"] + }; + native.SetTag("TestTagKey", "TestTagValue"); + native.SetExtra("TestExtraKey", "TestExtraValue"); + native.Breadcrumbs = + [ + new JavaSdk.Breadcrumb + { + Category = "category", + Level = JavaSdk.SentryLevel.Debug + } + ]; + + native.User = new JavaSdk.Protocol.User + { + Id = "user id", + Username = "test", + Email = "test@sentry.io", + IpAddress = "127.0.0.1" + }; + + var managed = native.ToSentryEvent(new JavaSdk.SentryOptions()); + + AssertEqual(managed, native); + } + + + private static void AssertEqual(SentryEvent managed, JavaSdk.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.Release.Should().Be(managed.Release, "Release"); + native.Environment.Should().Be(managed.Environment, "Environment"); + native.Transaction.Should().Be(managed.TransactionName!, "Transaction"); + native.Level!.ToString().ToUpper().Should().Be(managed.Level!.ToString()!.ToUpper(), "Level"); + // native.Throwable.Message.Should().Be(managed.Exception!.Message, "Message should match"); + + // extras + native.Extras.Should().NotBeNull("No extras found"); + native.Extras!.Count.Should().Be(1, "Extras should have 1 item"); + native.Extras!.Keys!.First().Should().Be(managed.Extra.Keys.First(), "Extras key should match"); + native.Extras!.Values!.First().ToString().Should().Be(managed.Extra.Values.First().ToString(), "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!.First().Should().Be(managed.Tags.Keys.First()); + native.Tags!.Values!.First().Should().Be(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!.Id.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"); + } +}