Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Trimming for .NET 7+ #2699

Closed
wants to merge 21 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
8f81603
Conditionally include Ben.Demystifier only if IsTrimmable != true
jamescrosswell Oct 5, 2023
8c8615e
Review feedback
jamescrosswell Oct 9, 2023
edb9e38
Removed automatic registration of WinUIUnhandledExceptionIntegration …
jamescrosswell Oct 9, 2023
48c3f0a
Merge branch 'feat/4.0.0' into feat/4.0.0-sentry-trimmable
jamescrosswell Oct 9, 2023
5d2a51b
Merged DebugStackTrace from feat/4.0.0
jamescrosswell Oct 10, 2023
9ccc529
Checkpoint
jamescrosswell Oct 10, 2023
dcb7441
Compiles (serialization verify tests still fail)
jamescrosswell Oct 10, 2023
4f6644c
Added some tests for JsonText source generated serializers
jamescrosswell Oct 11, 2023
5479f1e
Added SentryOptions.AddJsonSerializerContext
jamescrosswell Oct 11, 2023
98a51c7
Update SerializationTests.verify.cs
jamescrosswell Oct 11, 2023
dc2c522
Update DebugStackTrace.cs
jamescrosswell Oct 11, 2023
a216116
Fixed JsonSerializerContext with CustomConverters
jamescrosswell Oct 11, 2023
3b24c7b
Update ContextsTests.cs
jamescrosswell Oct 11, 2023
ae56436
Update JsonExtensions.cs
jamescrosswell Oct 11, 2023
bfb0818
Added support for a SentryJsonContext as well as a user defined JsonS…
jamescrosswell Oct 11, 2023
7322b6a
Fixed almost all of the JsonTests
jamescrosswell Oct 11, 2023
bb41fbb
Resolved deserialization issues for GraphQLRequestContent
jamescrosswell Oct 12, 2023
c37926c
Fixed MS Build issues
jamescrosswell Oct 12, 2023
29818c7
Update SentryClientTests.CaptureEvent_BeforeEventThrows_ErrorToEventB…
jamescrosswell Oct 12, 2023
adabd5d
Fixed CapturesEventWithContextKey_Implementation test
jamescrosswell Oct 12, 2023
89684a2
Update JsonExtensions.cs
jamescrosswell Oct 12, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Sentry.sln.DotSettings
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=QL/@EntryIndexedValue">QL</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=UI/@EntryIndexedValue">UI</s:String>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Enricher/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=enrichers/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=instrumenter/@EntryIndexedValue">True</s:Boolean>
Expand Down
66 changes: 64 additions & 2 deletions src/Sentry/GraphQLRequestContent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,7 @@ public GraphQLRequestContent(string? requestContent, SentryOptions? options = nu

try
{
var deserialized = JsonSerializer.Deserialize<Dictionary<string, object>>(requestContent, SerializerOptions);
Items = (deserialized ?? new Dictionary<string, object>()).AsReadOnly();
Items = GraphQLRequestContentReader.Read(requestContent);
}
catch (Exception e)
{
Expand Down Expand Up @@ -82,3 +81,66 @@ public GraphQLRequestContent(string? requestContent, SentryOptions? options = nu
/// </summary>
public string OperationTypeOrFallback() => OperationType ?? "graphql.operation";
}

/// <summary>
/// Adapted from https://github.com/graphql-dotnet/graphql-dotnet/blob/42a299e77748ec588bf34c33334e985098563298/src/GraphQL.SystemTextJson/GraphQLRequestJsonConverter.cs#L64
/// </summary>
internal static class GraphQLRequestContentReader
{
/// <summary>
/// Name for the operation name parameter.
/// See https://github.com/graphql/graphql-over-http/blob/master/spec/GraphQLOverHTTP.md#request-parameters
/// </summary>
private const string OperationNameKey = "operationName";

/// <summary>
/// Name for the query parameter.
/// See https://github.com/graphql/graphql-over-http/blob/master/spec/GraphQLOverHTTP.md#request-parameters
/// </summary>
private const string QueryKey = "query";


public static IReadOnlyDictionary<string, object> 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<string, object>();

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");
}
}
8 changes: 8 additions & 0 deletions src/Sentry/Integrations/WinUIUnhandledExceptionIntegration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -73,6 +75,11 @@ public void Register(IHub hub, SentryOptions options)
});
}

#if !TRIMMABLE
/// <summary>
/// This method uses reflection to hook up an UnhandledExceptionHandler. When IsTrimmed is true, users will have
/// follow our guidance to perform this initialization manually.
/// </summary>
private void AttachEventHandler()
{
try
Expand All @@ -92,6 +99,7 @@ private void AttachEventHandler()
_options.LogError("Could not attach WinUIUnhandledExceptionHandler.", ex);
}
}
#endif

private void WinUIUnhandledExceptionHandler(object sender, object e)
{
Expand Down
29 changes: 14 additions & 15 deletions src/Sentry/Internal/DebugStackTrace.cs
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,9 @@ private IEnumerable<SentryStackFrame> 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<StackFrame>'.
#if NETCOREAPP3_0
Expand Down Expand Up @@ -196,7 +198,7 @@ private IEnumerable<SentryStackFrame> CreateFrames(StackTrace stackTrace, bool i

/// <summary>
/// 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.
/// </summary>
private SentryStackFrame? TryCreateNativeAOTFrame(IStackFrame stackFrame)
Expand All @@ -213,7 +215,7 @@ private IEnumerable<SentryStackFrame> 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)
{
Expand Down Expand Up @@ -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();
Expand All @@ -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)
Expand Down Expand Up @@ -345,22 +345,21 @@ 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)
// is already prepended to the function, after return type.
// 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;
}

Expand Down
134 changes: 104 additions & 30 deletions src/Sentry/Internal/Extensions/JsonExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,40 +14,84 @@ internal static class JsonExtensions
new UIntPtrNullableJsonConverter()
};

private static List<JsonConverter> CustomConverters = new List<JsonConverter>();

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<JsonSerializerContext> DefaultSerializerContexts = new();
private static List<JsonSerializerContext> ReferencePreservingSerializerContexts = new();

private static List<Func<JsonSerializerOptions, JsonSerializerContext>> JsonSerializerContextBuilders = new()
{
options => new SentryJsonContext(options)
};

internal static void AddJsonSerializerContext<T>(Func<JsonSerializerOptions, T> 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)
{
Expand All @@ -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;
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -791,3 +857,11 @@ public static void WriteString(
}
}
}

#if TRIMMABLE
[JsonSerializable(typeof(GrowableArray<int>))]
[JsonSerializable(typeof(Dictionary<string, bool>))]
internal partial class SentryJsonContext : JsonSerializerContext
{
}
#endif
Loading
Loading