diff --git a/Sentry.sln.DotSettings b/Sentry.sln.DotSettings index 817e3366d3..7345707b8d 100644 --- a/Sentry.sln.DotSettings +++ b/Sentry.sln.DotSettings @@ -1,4 +1,6 @@  + QL + UI True True True diff --git a/src/Sentry/GraphQLRequestContent.cs b/src/Sentry/GraphQLRequestContent.cs index 865e8d11b1..44d358bde8 100644 --- a/src/Sentry/GraphQLRequestContent.cs +++ b/src/Sentry/GraphQLRequestContent.cs @@ -25,8 +25,7 @@ public GraphQLRequestContent(string? requestContent, SentryOptions? options = nu try { - var deserialized = JsonSerializer.Deserialize>(requestContent, SerializerOptions); - Items = (deserialized ?? new Dictionary()).AsReadOnly(); + Items = GraphQLRequestContentReader.Read(requestContent); } catch (Exception e) { @@ -82,3 +81,66 @@ public GraphQLRequestContent(string? requestContent, SentryOptions? options = nu /// public string OperationTypeOrFallback() => OperationType ?? "graphql.operation"; } + +/// +/// Adapted from https://github.com/graphql-dotnet/graphql-dotnet/blob/42a299e77748ec588bf34c33334e985098563298/src/GraphQL.SystemTextJson/GraphQLRequestJsonConverter.cs#L64 +/// +internal static class GraphQLRequestContentReader +{ + /// + /// Name for the operation name parameter. + /// See https://github.com/graphql/graphql-over-http/blob/master/spec/GraphQLOverHTTP.md#request-parameters + /// + private const string OperationNameKey = "operationName"; + + /// + /// Name for the query parameter. + /// See https://github.com/graphql/graphql-over-http/blob/master/spec/GraphQLOverHTTP.md#request-parameters + /// + private const string QueryKey = "query"; + + + public static IReadOnlyDictionary Read(string requestContent) + { + Utf8JsonReader reader = new(Encoding.UTF8.GetBytes(requestContent)); + if (!reader.Read() || reader.TokenType != JsonTokenType.StartObject) + { + throw new JsonException("Expected start of object"); + } + + var request = new Dictionary(); + + while (reader.Read()) + { + if (reader.TokenType == JsonTokenType.EndObject) + { + return request; + } + + if (reader.TokenType != JsonTokenType.PropertyName) + { + throw new JsonException("Expected property name"); + } + + var key = reader.GetString()!; + + if (!reader.Read()) + { + throw new JsonException("unexpected end of data"); + } + + switch (key) + { + case QueryKey: + case OperationNameKey: + request[key] = reader.GetString()!; + break; + default: + reader.Skip(); + break; + } + } + + throw new JsonException("unexpected end of data"); + } +} diff --git a/src/Sentry/Integrations/WinUIUnhandledExceptionIntegration.cs b/src/Sentry/Integrations/WinUIUnhandledExceptionIntegration.cs index 6690217054..d5a82a33d1 100644 --- a/src/Sentry/Integrations/WinUIUnhandledExceptionIntegration.cs +++ b/src/Sentry/Integrations/WinUIUnhandledExceptionIntegration.cs @@ -47,8 +47,10 @@ public void Register(IHub hub, SentryOptions options) _hub = hub; _options = options; +#if !TRIMMABLE // Hook the main event handler AttachEventHandler(); +#endif // First part of workaround for https://github.com/microsoft/microsoft-ui-xaml/issues/7160 AppDomain.CurrentDomain.FirstChanceException += (_, e) => _lastFirstChanceException = e.Exception; @@ -73,6 +75,11 @@ public void Register(IHub hub, SentryOptions options) }); } +#if !TRIMMABLE + /// + /// This method uses reflection to hook up an UnhandledExceptionHandler. When IsTrimmed is true, users will have + /// follow our guidance to perform this initialization manually. + /// private void AttachEventHandler() { try @@ -92,6 +99,7 @@ private void AttachEventHandler() _options.LogError("Could not attach WinUIUnhandledExceptionHandler.", ex); } } +#endif private void WinUIUnhandledExceptionHandler(object sender, object e) { diff --git a/src/Sentry/Internal/DebugStackTrace.cs b/src/Sentry/Internal/DebugStackTrace.cs index 95c4e2c1ec..20cbbec663 100644 --- a/src/Sentry/Internal/DebugStackTrace.cs +++ b/src/Sentry/Internal/DebugStackTrace.cs @@ -126,7 +126,9 @@ private IEnumerable CreateFrames(StackTrace stackTrace, bool i { var frames = _options.StackTraceMode switch { +#if !TRIMMABLE StackTraceMode.Enhanced => EnhancedStackTrace.GetFrames(stackTrace).Select(p => new RealStackFrame(p)), +#endif _ => stackTrace.GetFrames() // error CS8619: Nullability of reference types in value of type 'StackFrame?[]' doesn't match target type 'IEnumerable'. #if NETCOREAPP3_0 @@ -196,7 +198,7 @@ private IEnumerable CreateFrames(StackTrace stackTrace, bool i /// /// Native AOT implementation of CreateFrame. - /// Native frames have only limited method information at runtime (and even that can be disabled). + /// Native frames have only limited method information at runtime (and even that can be disabled). /// We try to parse that and also add addresses for server-side symbolication. /// private SentryStackFrame? TryCreateNativeAOTFrame(IStackFrame stackFrame) @@ -213,7 +215,7 @@ private IEnumerable CreateFrames(StackTrace stackTrace, bool i } // Method info is currently only exposed by ToString(), see https://github.com/dotnet/runtime/issues/92869 - // We only care about the case where the method is available (`StackTraceSupport` property is the default `true`): + // We only care about the case where the method is available (`StackTraceSupport` property is the default `true`): // https://github.com/dotnet/runtime/blob/254230253da143a082f47cfaf8711627c0bf2faf/src/coreclr/nativeaot/System.Private.CoreLib/src/Internal/DeveloperExperience/DeveloperExperience.cs#L42 internal static SentryStackFrame ParseNativeAOTToString(string info) { @@ -241,9 +243,10 @@ internal static SentryStackFrame ParseNativeAOTToString(string info) var frame = new SentryStackFrame { Module = method.DeclaringType?.FullName ?? unknownRequiredField, - Package = method.DeclaringType?.Assembly.FullName + Package = method.DeclaringType?.Assembly.FullName, + Function = method.Name }; - +#if !TRIMMABLE if (stackFrame.Frame is EnhancedStackFrame enhancedStackFrame) { var stringBuilder = new StringBuilder(); @@ -263,10 +266,7 @@ internal static SentryStackFrame ParseNativeAOTToString(string info) : module; } } - else - { - frame.Function = method.Name; - } +#endif // Originally we didn't skip methods from dynamic assemblies, so not to break compatibility: if (_options.StackTraceMode != StackTraceMode.Original && method.Module.Assembly.IsDynamic) @@ -345,13 +345,7 @@ internal static SentryStackFrame ParseNativeAOTToString(string info) frame.ColumnNumber = colNo; } - if (stackFrame.Frame is not EnhancedStackFrame) - { - DemangleAsyncFunctionName(frame); - DemangleAnonymousFunction(frame); - DemangleLambdaReturnType(frame); - } - +#if !TRIMMABLE if (stackFrame.Frame is EnhancedStackFrame) { // In Enhanced mode, Module (which in this case is the Namespace) @@ -359,8 +353,13 @@ internal static SentryStackFrame ParseNativeAOTToString(string info) // Removing here at the end because this is used to resolve InApp=true/false // TODO what is this really about? we have already run ConfigureAppFrame() at this time... frame.Module = null; + return frame; } +#endif + DemangleAsyncFunctionName(frame); + DemangleAnonymousFunction(frame); + DemangleLambdaReturnType(frame); return frame; } diff --git a/src/Sentry/Internal/Extensions/JsonExtensions.cs b/src/Sentry/Internal/Extensions/JsonExtensions.cs index 440426d6f0..0f7d5cdbe3 100644 --- a/src/Sentry/Internal/Extensions/JsonExtensions.cs +++ b/src/Sentry/Internal/Extensions/JsonExtensions.cs @@ -14,40 +14,84 @@ internal static class JsonExtensions new UIntPtrNullableJsonConverter() }; + private static List CustomConverters = new List(); + internal static bool JsonPreserveReferences { get; set; } = true; - private static JsonSerializerOptions SerializerOptions = null!; - private static JsonSerializerOptions AltSerializerOptions = null!; static JsonExtensions() { ResetSerializerOptions(); } - internal static void ResetSerializerOptions() + private static JsonSerializerOptions BuildOptions(bool preserveReferences) + { + var options = new JsonSerializerOptions(); + if (preserveReferences) + { + options.ReferenceHandler = ReferenceHandler.Preserve; + } + foreach (var converter in DefaultConverters) + { + options.Converters.Add(converter); + } + foreach (var converter in CustomConverters) + { + options.Converters.Add(converter); + } + + return options; + } + +#if TRIMMABLE + private static List DefaultSerializerContexts = new(); + private static List ReferencePreservingSerializerContexts = new(); + + private static List> JsonSerializerContextBuilders = new() + { + options => new SentryJsonContext(options) + }; + + internal static void AddJsonSerializerContext(Func jsonSerializerContextBuilder) + where T: JsonSerializerContext { - SerializerOptions = new JsonSerializerOptions() - .AddDefaultConverters(); + JsonSerializerContextBuilders.Add(jsonSerializerContextBuilder); + ResetSerializerOptions(); + } - AltSerializerOptions = new JsonSerializerOptions + internal static void ResetSerializerOptions() + { + DefaultSerializerContexts.Clear(); + ReferencePreservingSerializerContexts.Clear(); + foreach (var builder in JsonSerializerContextBuilders) { - ReferenceHandler = ReferenceHandler.Preserve + DefaultSerializerContexts.Add(builder(BuildOptions(false))); + ReferencePreservingSerializerContexts.Add(builder(BuildOptions(true))); } - .AddDefaultConverters(); } +#else + private static JsonSerializerOptions SerializerOptions = null!; + private static JsonSerializerOptions AltSerializerOptions = null!; + + internal static void ResetSerializerOptions() + { + SerializerOptions = BuildOptions(false); + AltSerializerOptions = BuildOptions(true); + } +#endif + internal static void AddJsonConverter(JsonConverter converter) { // only add if we don't have this instance already - var converters = SerializerOptions.Converters; - if (converters.Contains(converter)) + if (CustomConverters.Contains(converter)) { return; } try { - SerializerOptions.Converters.Add(converter); - AltSerializerOptions.Converters.Add(converter); + CustomConverters.Add(converter); + ResetSerializerOptions(); } catch (InvalidOperationException) { @@ -62,16 +106,6 @@ internal static void AddJsonConverter(JsonConverter converter) } } - private static JsonSerializerOptions AddDefaultConverters(this JsonSerializerOptions options) - { - foreach (var converter in DefaultConverters) - { - options.Converters.Add(converter); - } - - return options; - } - public static void Deconstruct(this JsonProperty jsonProperty, out string name, out JsonElement value) { name = jsonProperty.Name; @@ -477,31 +511,63 @@ public static void WriteDynamicValue( { writer.WriteStringValue(formattable.ToString(null, CultureInfo.InvariantCulture)); } + else if (value.GetType().ToString() == "System.RuntimeType") + { + writer.WriteStringValue(value.ToString()); + } else { if (!JsonPreserveReferences) { - JsonSerializer.Serialize(writer, value, SerializerOptions); + InternalSerialize(writer, value, preserveReferences: false); return; } try { - // Use an intermediate temporary stream, so we can retry if serialization fails. - using var tempStream = new MemoryStream(); - using var tempWriter = new Utf8JsonWriter(tempStream, writer.Options); - JsonSerializer.Serialize(tempWriter, value, SerializerOptions); - tempWriter.Flush(); - writer.WriteRawValue(tempStream.ToArray()); + // Use an intermediate byte array, so we can retry if serialization fails. + var bytes = InternalSerializeToUtf8Bytes(value); + writer.WriteRawValue(bytes); } catch (JsonException) { // Retry, preserving references to avoid cyclical dependency. - JsonSerializer.Serialize(writer, value, AltSerializerOptions); + InternalSerialize(writer, value, preserveReferences: true); } } } +#if TRIMMABLE + + private static JsonSerializerContext GetSerializerContext(Type type, bool preserveReferences = false) + { + var contexts = preserveReferences ? ReferencePreservingSerializerContexts : DefaultSerializerContexts; + return contexts.FirstOrDefault(c => c.GetTypeInfo(type) != null) + ?? contexts[0]; // If none of the contexts has type info, this gives us a proper exception message + } + + private static byte[] InternalSerializeToUtf8Bytes(object value) + { + var context = GetSerializerContext(value.GetType()); + return JsonSerializer.SerializeToUtf8Bytes(value, value.GetType(), context); + } + + private static void InternalSerialize(Utf8JsonWriter writer, object value, bool preserveReferences = false) + { + var context = GetSerializerContext(value.GetType(), preserveReferences); + JsonSerializer.Serialize(writer, value, value.GetType(), context); + } +#else + private static byte[] InternalSerializeToUtf8Bytes(object value) => + JsonSerializer.SerializeToUtf8Bytes(value, SerializerOptions); + + private static void InternalSerialize(Utf8JsonWriter writer, object value, bool preserveReferences = false) + { + var options = preserveReferences ? AltSerializerOptions : SerializerOptions; + JsonSerializer.Serialize(writer, value, options); + } +#endif + public static void WriteDynamic( this Utf8JsonWriter writer, string propertyName, @@ -791,3 +857,11 @@ public static void WriteString( } } } + +#if TRIMMABLE +[JsonSerializable(typeof(GrowableArray))] +[JsonSerializable(typeof(Dictionary))] +internal partial class SentryJsonContext : JsonSerializerContext +{ +} +#endif diff --git a/src/Sentry/Sentry.csproj b/src/Sentry/Sentry.csproj index f790512a03..ec18456b43 100644 --- a/src/Sentry/Sentry.csproj +++ b/src/Sentry/Sentry.csproj @@ -22,21 +22,27 @@ - - - - %(RecursiveDir)\%(Filename)%(Extension) - - - + + true + $(DefineConstants);TRIMMABLE + + + + + + %(RecursiveDir)\%(Filename)%(Extension) + + + - - - - + + + + + diff --git a/src/Sentry/SentryClient.cs b/src/Sentry/SentryClient.cs index d0d52dfe7e..ce7669d2e1 100644 --- a/src/Sentry/SentryClient.cs +++ b/src/Sentry/SentryClient.cs @@ -166,8 +166,10 @@ public void CaptureTransaction(Transaction transaction, Hint? hint) } catch (Exception e) { +#if !TRIMMABLE // Attempt to demystify exceptions before adding them as breadcrumbs. e.Demystify(); +#endif _options.LogError("The BeforeSendTransaction callback threw an exception. It will be added as breadcrumb and continue.", e); @@ -369,8 +371,10 @@ private bool CaptureEnvelope(Envelope envelope) } catch (Exception e) { +#if !TRIMMABLE // Attempt to demystify exceptions before adding them as breadcrumbs. e.Demystify(); +#endif _options.LogError("The BeforeSend callback threw an exception. It will be added as breadcrumb and continue.", e); var data = new Dictionary diff --git a/src/Sentry/SentryOptions.cs b/src/Sentry/SentryOptions.cs index 5c0d653b6b..eb208a9baf 100644 --- a/src/Sentry/SentryOptions.cs +++ b/src/Sentry/SentryOptions.cs @@ -986,6 +986,32 @@ public void AddJsonConverter(JsonConverter converter) JsonExtensions.AddJsonConverter(converter); } +#if TRIMMABLE + /// + /// Configures a custom to be used when serializing or deserializing + /// objects to JSON with this SDK. + /// + /// + /// A builder that takes and returns a + /// + /// + /// This currently modifies a static list, so will affect any instance of the Sentry SDK. + /// If that becomes problematic, we will have to refactor all serialization code to be + /// able to accept an instance of . + /// + public void AddJsonSerializerContext(Func contextBuilder) + where T: JsonSerializerContext + { + // protect against null because user may not have nullability annotations enabled + if (contextBuilder == null!) + { + throw new ArgumentNullException(nameof(contextBuilder)); + } + + JsonExtensions.AddJsonSerializerContext(contextBuilder); + } +#endif + /// /// When true, if an object being serialized to JSON contains references to other objects, and the /// serialized object graph exceed the maximum allowable depth, the object will instead be serialized using @@ -1085,7 +1111,7 @@ public SentryOptions() #endif }; -#if NET5_0_OR_GREATER +#if NET5_0_OR_GREATER && !TRIMMABLE if (WinUIUnhandledExceptionIntegration.IsApplicable) { this.AddIntegration(new WinUIUnhandledExceptionIntegration()); diff --git a/test/Sentry.Tests/HubTests.cs b/test/Sentry.Tests/HubTests.cs index e175eaf694..97cef588fa 100644 --- a/test/Sentry.Tests/HubTests.cs +++ b/test/Sentry.Tests/HubTests.cs @@ -275,7 +275,7 @@ public void CaptureEvent_ExceptionWithOpenSpan_SpanLinkedToEventContext() Assert.Equal(child.ParentSpanId, evt.Contexts.Trace.ParentSpanId); } - private class EvilContext + internal class EvilContext { // This property will throw an exception during serialization. // ReSharper disable once UnusedMember.Local @@ -300,6 +300,9 @@ await TestHelpers.RetryTestAsync( private async Task CapturesEventWithContextKey_Implementation(bool offlineCaching) { +#if NET7_0_OR_GREATER + JsonExtensions.AddJsonSerializerContext(o => new HubTestsJsonContext(o)); +#endif var tcs = new TaskCompletionSource(); var expectedMessage = Guid.NewGuid().ToString(); @@ -1523,3 +1526,10 @@ public async Task WithScopeAsyncT_Works() private static Scope GetCurrentScope(Hub hub) => hub.ScopeManager.GetCurrent().Key; } + +#if NET7_0_OR_GREATER +[JsonSerializable(typeof(HubTests.EvilContext))] +internal partial class HubTestsJsonContext : JsonSerializerContext +{ +} +#endif diff --git a/test/Sentry.Tests/Internals/DebugStackTraceTests.verify.cs b/test/Sentry.Tests/Internals/DebugStackTraceTests.verify.cs index 36d7b62162..b4e86a6d1f 100644 --- a/test/Sentry.Tests/Internals/DebugStackTraceTests.verify.cs +++ b/test/Sentry.Tests/Internals/DebugStackTraceTests.verify.cs @@ -40,16 +40,25 @@ public void CreateSentryStackFrame_AppNamespaceExcluded_NotInAppFrame() Assert.False(actual?.InApp); } +#if !TEST_TRIMMABLE [Theory] [InlineData(true)] [InlineData(false)] public void CreateSentryStackFrame_SystemType_NotInAppFrame(bool useEnhancedStackTrace) +#else + [Fact] + public void CreateSentryStackFrame_SystemType_NotInAppFrame() +#endif { // Arrange var sut = _fixture.GetSut(); var exception = Assert.ThrowsAny(() => _ = Convert.FromBase64String("This will throw.")); var stackTrace = new StackTrace(exception); +#if !TEST_TRIMMABLE var frame = useEnhancedStackTrace ? EnhancedStackTrace.GetFrames(stackTrace)[0] : stackTrace.GetFrame(0); +#else + var frame = stackTrace.GetFrame(0); +#endif // Sanity Check Assert.NotNull(frame); diff --git a/test/Sentry.Tests/Internals/JsonTests.cs b/test/Sentry.Tests/Internals/JsonTests.cs index 4214d759b4..b250c219a6 100644 --- a/test/Sentry.Tests/Internals/JsonTests.cs +++ b/test/Sentry.Tests/Internals/JsonTests.cs @@ -21,7 +21,7 @@ public static Exception GenerateException(string description) } } - private class DataAndNonSerializableObject + internal class DataAndNonSerializableObject { /// /// A class containing two objects that can be serialized and a third one that will have issues if serialized. @@ -60,7 +60,7 @@ private class ExceptionObjectMock public int? HResult { get; set; } } - private class DataWithSerializableObject : DataAndNonSerializableObject + internal class DataWithSerializableObject : DataAndNonSerializableObject { /// /// A class containing three objects that can be serialized. @@ -73,6 +73,9 @@ public DataWithSerializableObject(T obj) : base(obj) { } public void WriteDynamicValue_ExceptionParameter_SerializedException() { // Arrange +#if NET7_0_OR_GREATER + JsonExtensions.AddJsonSerializerContext(o => new JsonTestsJsonContext(o)); +#endif var ex = GenerateException("Test"); ex.Data.Add("a", "b"); @@ -107,6 +110,9 @@ public void WriteDynamicValue_ExceptionParameter_SerializedException() public void WriteDynamicValue_ClassWithExceptionParameter_SerializedClassWithException() { // Arrange +#if NET7_0_OR_GREATER + JsonExtensions.AddJsonSerializerContext(o => new JsonTestsJsonContext(o)); +#endif var expectedMessage = "T est"; var expectedData = new KeyValuePair("a", "b"); var ex = GenerateException(expectedMessage); @@ -135,7 +141,11 @@ public void WriteDynamicValue_ClassWithExceptionParameter_SerializedClassWithExc [Fact] public void WriteDynamicValue_TypeParameter_FullNameTypeOutput() { +#if NET7_0_OR_GREATER + JsonExtensions.AddJsonSerializerContext(o => new JsonTestsJsonContext(o)); +#endif // Arrange + // TODO: Fix "Metadata for type 'System.RuntimeType' was not provided by TypeInfoResolver"... var type = typeof(Exception); var expectedValue = "\"System.Exception\""; @@ -150,6 +160,9 @@ public void WriteDynamicValue_TypeParameter_FullNameTypeOutput() public void WriteDynamicValue_ClassWithTypeParameter_ClassFormatted() { // Arrange +#if NET7_0_OR_GREATER + JsonExtensions.AddJsonSerializerContext(o => new JsonTestsJsonContext(o)); +#endif var type = typeof(List<>).GetGenericArguments()[0]; var data = new DataWithSerializableObject(type); @@ -165,6 +178,9 @@ public void WriteDynamicValue_ClassWithTypeParameter_ClassFormatted() public void WriteDynamicValue_ClassWithAssembly_SerializedClassWithNullAssembly() { // Arrange +#if NET7_0_OR_GREATER + JsonExtensions.AddJsonSerializerContext(o => new JsonTestsJsonContext(o)); +#endif var data = new DataAndNonSerializableObject(AppDomain.CurrentDomain.GetAssemblies()[0]); // Act @@ -205,6 +221,9 @@ public void WriteDynamic_ComplexObject_PreserveReferences() { // Arrange JsonExtensions.JsonPreserveReferences = true; +#if NET7_0_OR_GREATER + JsonExtensions.AddJsonSerializerContext(o => new JsonTestsJsonContext(o)); +#endif var testObject = new SelfReferencedObject(); using var stream = new MemoryStream(); using (var writer = new Utf8JsonWriter(stream)) @@ -242,3 +261,15 @@ public class SelfReferencedObject public SelfReferencedObject Object => this; } } + +#if NET7_0_OR_GREATER +[JsonSerializable(typeof(AccessViolationException))] +[JsonSerializable(typeof(Exception))] +[JsonSerializable(typeof(JsonTests.SelfReferencedObject))] +[JsonSerializable(typeof(JsonTests.DataAndNonSerializableObject))] +[JsonSerializable(typeof(JsonTests.DataWithSerializableObject))] +[JsonSerializable(typeof(JsonTests.DataWithSerializableObject))] +internal partial class JsonTestsJsonContext : JsonSerializerContext +{ +} +#endif diff --git a/test/Sentry.Tests/Internals/SentryStackTraceFactoryTests.cs b/test/Sentry.Tests/Internals/SentryStackTraceFactoryTests.cs index 665d2650f9..6e5df28525 100644 --- a/test/Sentry.Tests/Internals/SentryStackTraceFactoryTests.cs +++ b/test/Sentry.Tests/Internals/SentryStackTraceFactoryTests.cs @@ -47,7 +47,9 @@ public void Create_NoExceptionAndAttachStackTraceOptionOnWithOriginalMode_Curren [Theory] [InlineData(StackTraceMode.Original, "AsyncWithWait_StackTrace { }")] +#if !TEST_TRIMMABLE [InlineData(StackTraceMode.Enhanced, "void SentryStackTraceFactoryTests.AsyncWithWait_StackTrace(StackTraceMode mode, string method)+() => { }")] +#endif public void AsyncWithWait_StackTrace(StackTraceMode mode, string method) { _fixture.SentryOptions.AttachStacktrace = true; @@ -69,7 +71,9 @@ public void AsyncWithWait_StackTrace(StackTraceMode mode, string method) [Theory] [InlineData(StackTraceMode.Original, "MoveNext")] // Should be "AsyncWithAwait_StackTrace { }", but see note in SentryStackTraceFactory +#if !TEST_TRIMMABLE [InlineData(StackTraceMode.Enhanced, "async Task SentryStackTraceFactoryTests.AsyncWithAwait_StackTrace(StackTraceMode mode, string method)+(?) => { }")] +#endif public async Task AsyncWithAwait_StackTrace(StackTraceMode mode, string method) { _fixture.SentryOptions.AttachStacktrace = true; @@ -87,6 +91,7 @@ public async Task AsyncWithAwait_StackTrace(StackTraceMode mode, string method) Assert.Equal(method, stackTrace.Frames.Last().Function); } +#if !TEST_TRIMMABLE [Fact] public void Create_NoExceptionAndAttachStackTraceOptionOnWithEnhancedMode_CurrentStackTrace() { @@ -108,6 +113,7 @@ public void Create_NoExceptionAndAttachStackTraceOptionOnWithEnhancedMode_Curren StringComparison.Ordinal ) == true); } +#endif [Fact] public void Create_WithExceptionAndDefaultAttachStackTraceOption_HasStackTrace() @@ -185,7 +191,9 @@ public void FileNameShouldBeRelative() [Theory] [InlineData(StackTraceMode.Original, "ByRefMethodThatThrows")] +#if !TEST_TRIMMABLE [InlineData(StackTraceMode.Enhanced, "(Fixture f, int b) SentryStackTraceFactoryTests.ByRefMethodThatThrows(int value, in int valueIn, ref int valueRef, out int valueOut)")] +#endif public void Create_InlineCase_IncludesAmpersandAfterParameterType(StackTraceMode mode, string method) { _fixture.SentryOptions.StackTraceMode = mode; diff --git a/test/Sentry.Tests/Internals/SentryStackTraceFactoryTests.verify.cs b/test/Sentry.Tests/Internals/SentryStackTraceFactoryTests.verify.cs index e701641303..b5af743458 100644 --- a/test/Sentry.Tests/Internals/SentryStackTraceFactoryTests.verify.cs +++ b/test/Sentry.Tests/Internals/SentryStackTraceFactoryTests.verify.cs @@ -9,7 +9,9 @@ public partial class SentryStackTraceFactoryTests { [SkippableTheory] [InlineData(StackTraceMode.Original)] +#if !TEST_TRIMMABLE [InlineData(StackTraceMode.Enhanced)] +#endif public Task MethodGeneric(StackTraceMode mode) { // TODO: Mono gives different results. Investigate why. diff --git a/test/Sentry.Tests/Protocol/Context/ContextsTests.cs b/test/Sentry.Tests/Protocol/Context/ContextsTests.cs index 204cc264c0..dcca5a39f7 100644 --- a/test/Sentry.Tests/Protocol/Context/ContextsTests.cs +++ b/test/Sentry.Tests/Protocol/Context/ContextsTests.cs @@ -137,6 +137,7 @@ public void SerializeObject_SingleRuntimePropertySet_SerializeSingleProperty() Assert.Equal("""{"runtime":{"type":"runtime","version":"2.1.1.100"}}""", actualString); } +#if !TEST_TRIMMABLE [Fact] public void SerializeObject_AnonymousObject_SerializedCorrectly() { @@ -159,6 +160,7 @@ public void SerializeObject_AnonymousObject_SerializedCorrectly() ["Baz"] = "kek" }); } +#endif [Fact] public void SerializeObject_SortsContextKeys() diff --git a/test/Sentry.Tests/Sentry.Tests.csproj b/test/Sentry.Tests/Sentry.Tests.csproj index a0bdebbb3b..01a433ed67 100644 --- a/test/Sentry.Tests/Sentry.Tests.csproj +++ b/test/Sentry.Tests/Sentry.Tests.csproj @@ -7,8 +7,24 @@ $(TargetFrameworks);net7.0-maccatalyst - + + $(DefineConstants);TEST_TRIMMABLE + $(NoWarn);SYSLIB0012;SYSLIB0005 + + + + + + + + + + + + + + diff --git a/test/Sentry.Tests/SentryClientTests.CaptureEvent_BeforeEventThrows_ErrorToEventBreadcrumb.verified.txt b/test/Sentry.Tests/SentryClientTests.CaptureEvent_BeforeEventThrows_ErrorToEventBreadcrumb.verified.txt index 6ce33e1e01..8ae43ba2f9 100644 --- a/test/Sentry.Tests/SentryClientTests.CaptureEvent_BeforeEventThrows_ErrorToEventBreadcrumb.verified.txt +++ b/test/Sentry.Tests/SentryClientTests.CaptureEvent_BeforeEventThrows_ErrorToEventBreadcrumb.verified.txt @@ -5,8 +5,8 @@ Data: { message: Exception message!, stackTrace: -at Task Sentry.Tests.SentryClientTests.CaptureEvent_BeforeEventThrows_ErrorToEventBreadcrumb() -at SentryEvent Sentry.SentryClient.BeforeSend(...) +at Sentry.Tests.SentryClientTests.<>c__DisplayClass71_0.b__0(...) +at Sentry.SentryClient.BeforeSend(...) }, Category: SentryClient, Level: error diff --git a/test/Sentry.Tests/SentryClientTests.verify.cs b/test/Sentry.Tests/SentryClientTests.verify.cs index 47c3d0a2bb..6edb8c5629 100644 --- a/test/Sentry.Tests/SentryClientTests.verify.cs +++ b/test/Sentry.Tests/SentryClientTests.verify.cs @@ -6,31 +6,35 @@ public partial class SentryClientTests [Fact] public Task CaptureEvent_BeforeEventThrows_ErrorToEventBreadcrumb() { + // Arrange var error = new Exception("Exception message!"); _fixture.SentryOptions.SetBeforeSend((_,_) => throw error); - var @event = new SentryEvent(); - var sut = _fixture.GetSut(); + + // Act _ = sut.CaptureEvent(@event); + // Assert return Verify(@event.Breadcrumbs); } [Fact] public Task CaptureTransaction_BeforeSendTransactionThrows_ErrorToEventBreadcrumb() { + // Arrange var error = new Exception("Exception message!"); _fixture.SentryOptions.SetBeforeSendTransaction((_, _) => throw error); - var transaction = new Transaction("name", "operation") { IsSampled = true }; - var sut = _fixture.GetSut(); + + // Act sut.CaptureTransaction(transaction); + // Assert return Verify(transaction.Breadcrumbs); } } diff --git a/test/Sentry.Tests/SerializationTests.verify.cs b/test/Sentry.Tests/SerializationTests.verify.cs index 37edd86544..9f6a61f508 100644 --- a/test/Sentry.Tests/SerializationTests.verify.cs +++ b/test/Sentry.Tests/SerializationTests.verify.cs @@ -1,3 +1,7 @@ +#if NET7_0_OR_GREATER +using System.Text.Json.Serialization.Metadata; +#endif + namespace Sentry.Tests; [UsesVerify] @@ -18,6 +22,39 @@ public async Task Serialization(string name, object target) await Verify(json).UseParameters(name); } +#if NET7_0_OR_GREATER + internal class NestedStringClass { public string Value { get; set; } } + internal class NestedIntClass { public int Value { get; set; } } + internal class NestedNIntClass { public nint Value { get; set; } } + internal class NestedNuIntClass { public nuint Value { get; set; } } + internal class NestedIntPtrClass { public IntPtr Value { get; set; } } + internal class NestedNullableIntPtrClass { public IntPtr? Value { get; set; } } + internal class NestedUIntPtrClass { public UIntPtr Value { get; set; } } + internal class NestedNullableUIntPtrClass { public UIntPtr? Value { get; set; } } + + public static IEnumerable GetData() + { + yield return new object[] { "string", "string value" }; + yield return new object[] { "int", 5 }; + + JsonExtensions.ResetSerializerOptions(); + JsonExtensions.AddJsonSerializerContext(options => new SerializationTestsJsonContext(options)); + yield return new object[] { "nested string", new NestedStringClass { Value= "string value" } }; + yield return new object[] { "nested int", new NestedIntClass { Value= 5 } }; + yield return new object[] { "nested nint", new NestedNIntClass { Value= 5 } }; + yield return new object[] { "nested nuint", new NestedNuIntClass { Value= 5 } }; + yield return new object[] { "nested IntPtr", new NestedIntPtrClass { Value= (IntPtr)3 } }; + yield return new object[] { "nested nullable IntPtr", new NestedNullableIntPtrClass { Value= (IntPtr?)3 } }; + yield return new object[] { "nested UIntPtr", new NestedUIntPtrClass { Value= (UIntPtr)3 } }; + yield return new object[] { "nested nullable UIntPtr", new NestedNullableUIntPtrClass { Value= (UIntPtr?)3 } }; + + JsonExtensions.ResetSerializerOptions(); + JsonExtensions.AddJsonConverter(new CustomObjectConverter()); + JsonExtensions.AddJsonSerializerContext(options => new SerializationTestsJsonContext(options)); + yield return new object[] {"custom object with value", new CustomObject("test")}; + yield return new object[] {"custom object with null", new CustomObject(null)}; + } +#else public static IEnumerable GetData() { yield return new object[] {"string", "string value"}; @@ -36,6 +73,7 @@ public static IEnumerable GetData() yield return new object[] {"custom object with value", new CustomObject("test")}; yield return new object[] {"custom object with null", new CustomObject(null)}; } +#endif public class CustomObject { @@ -56,3 +94,18 @@ public override void Write(Utf8JsonWriter writer, CustomObject value, JsonSerial => writer.WriteStringValue(value.Value); } } + +#if NET7_0_OR_GREATER +[JsonSerializable(typeof(SerializationTests.CustomObject))] +[JsonSerializable(typeof(SerializationTests.NestedStringClass))] +[JsonSerializable(typeof(SerializationTests.NestedIntClass))] +[JsonSerializable(typeof(SerializationTests.NestedNIntClass))] +[JsonSerializable(typeof(SerializationTests.NestedNuIntClass))] +[JsonSerializable(typeof(SerializationTests.NestedIntPtrClass))] +[JsonSerializable(typeof(SerializationTests.NestedNullableIntPtrClass))] +[JsonSerializable(typeof(SerializationTests.NestedUIntPtrClass))] +[JsonSerializable(typeof(SerializationTests.NestedNullableUIntPtrClass))] +internal partial class SerializationTestsJsonContext : JsonSerializerContext +{ +} +#endif