/// <summary> /// Sends a report to Sentry only (no log, no toast, etc.). /// Does not send to Sentry if ApplicationUpdateSupport.IsDev is true. /// Fails silently. /// </summary> /// <param name="message">The message to send with the report</param> /// <param name="fullDetailedMessage">An optional message which can be added to the Sentry report</param> public static void ReportSentryOnly(string message, string fullDetailedMessage = null) { if (ApplicationUpdateSupport.IsDev) { Debug.WriteLine("Developer, we though you might want to know that ReportSentryOnly() was called. Ignore if you like."); Debug.Indent(); Debug.WriteLine(message); Debug.WriteLine(fullDetailedMessage); Debug.Unindent(); return; } try { var evt = new SentryEvent { Message = message }; if (!string.IsNullOrWhiteSpace(fullDetailedMessage)) { evt.SetExtra("fullDetailedMessage", fullDetailedMessage); } evt.SetExtra("stackTrace", new StackTrace().ToString()); SentrySdk.CaptureEvent(evt); } catch (Exception err) { // will only "do something" if we're testing reporting and have thus turned off checking for dev Debug.Fail(err.Message); } }
private static SentryEvent SentryBeforeSend(SentryEvent args) { #if DEBUG return(null); #else if (args.Exception?.TargetSite.Module.Assembly == Engine.ClassicAssembly) { return(null); } args.User = new User { Id = AssistantOptions.UserId }; args.SetTag("SessionId", AssistantOptions.SessionId); args.SetExtra("PlayerName", Engine.Player?.Name ?? "Unknown"); args.SetExtra("PlayerSerial", Engine.Player?.Serial ?? 0); args.SetExtra("Shard", Engine.CurrentShard?.Name ?? "Unknown"); args.SetExtra("ShardFeatures", Engine.Features.ToString()); args.SetExtra("CharacterListFlags", Engine.CharacterListFlags.ToString()); args.SetExtra("Connected", Engine.Connected); args.SetExtra("ClientVersion", Engine.ClientVersion == null ? "Unknown" : Engine.ClientVersion.ToString()); args.SetExtra("KeyboardLayout", InputLanguageManager.Current?.CurrentInputLanguage?.Name ?? "Unknown"); args.SetExtra("ClassicUO Version", Engine.ClassicAssembly?.GetName().Version.ToString() ?? "Unknown"); return(args); #endif }
private static SentryEvent SentryBeforeSend(SentryEvent args) { args.User = new User { Id = AssistantOptions.UserId }; args.SetTag("SessionId", AssistantOptions.SessionId); args.SetExtra("PlayerName", Engine.Player?.Name ?? "Unknown"); args.SetExtra("PlayerSerial", Engine.Player?.Serial ?? 0); args.SetExtra("Shard", Engine.CurrentShard?.Name ?? "Unknown"); args.SetExtra("Connected", Engine.Connected); return(args); }
private SentryEvent PrepareEvent(SentryEvent @event) { var scope = ScopeManagement.GetCurrent(); // TODO: Consider multiple events being sent with the same scope: // Wherever this code will end up, it should evaluate only once if (scope.States != null) { foreach (var state in scope.States) { if (state is string scopeString) { @event.SetTag("scope", scopeString); } else if (state is IEnumerable <KeyValuePair <string, string> > keyValStringString) { @event.SetTags(keyValStringString); } else if (state is IEnumerable <KeyValuePair <string, object> > keyValStringObject) { @event.SetTags(keyValStringObject.Select(k => new KeyValuePair <string, string>(k.Key, k.Value.ToString()))); } else { // TODO: possible callback invocation here @event.SetExtra("State of unknown type", state.GetType().ToString()); } } } @event = _options.BeforeSend?.Invoke(@event); return(@event); }
private static void ConfigureServices(IServiceCollection services) { services.AddHttpClient <ISymbolClient, SymbolClient>() .AddPolicyHandler((s, r) => HttpPolicyExtensions.HandleTransientHttpError() .WaitAndRetryAsync(new[] { TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(2), TimeSpan.FromSeconds(5), #if RELEASE // TODO: Until a proper re-entrancy is built in the clients, add a last hope retry TimeSpan.FromSeconds(15) #endif }, onRetry: async(result, span, retryAttempt, context) => { var sentry = s.GetService <ISentryClient>(); var evt = new SentryEvent(result.Exception) { Level = SentryLevel.Warning, LogEntry = new LogEntry { Formatted = $"Waiting {span} following attempt {retryAttempt} failed HTTP request.", Message = "Waiting {span} following attempt {retryAttempt} failed HTTP request.", } }; evt.SetTag("Tag", "Polly"); if (result.Result is { } request) { const string traceIdKey = "TraceIdentifier"; if (request.Headers.TryGetValues(traceIdKey, out var traceIds)) { evt.SetTag(traceIdKey, traceIds.FirstOrDefault() ?? "unknown"); } evt.SetTag("StatusCode", request.StatusCode.ToString()); var responseBody = await request.Content.ReadAsStringAsync(); if (!string.IsNullOrWhiteSpace(responseBody)) { evt.SetExtra("body", responseBody); } } sentry.CaptureEvent(evt); } )); services.AddSingleton <Client>(); services.AddSingleton <ObjectFileParser>(); services.AddSingleton <ClientMetrics>(); services.AddSingleton <FatBinaryReader>(); services.AddSingleton <ClientMetrics>(); services.AddOptions <SymbolClientOptions>() .Configure <IConfiguration>((o, f) => f.Bind("SymbolClient", o)) .Validate(o => o.BaseAddress is {}, "BaseAddress is required.");
/// <inheritdoc /> public void Log <TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func <TState, Exception, string> formatter) { // Skip irrelevant logging levels if (!IsEnabled(logLevel)) { return; } // Case in which logger.LogError("message") is used if (exception == null) { exception = new Exception($"No exception thrown. Message included: {state}"); } // Log to sentry var sentryEvent = new SentryEvent(exception); sentryEvent.SetExtra("configuration", _serializedConfiguration); sentryEvent.SetExtra("algorithm_configuration", _serializedAlgorithmConfiguration); _sentryClient.CaptureEvent(sentryEvent); }
private static void FlattenException(Exception exception, SentryEvent sentryEvent, string key) { var properties = exception.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance); foreach (var propertyInfo in properties.Where(x => x.DeclaringType != typeof(Exception))) { try { var value = JsonConvert.SerializeObject(propertyInfo.GetValue(exception, null)); sentryEvent.SetExtra(key + "." + propertyInfo.Name, value); } catch (Exception ex) { sentryEvent.SetExtra(key + "." + propertyInfo.Name, "ERROR accessing value: " + ex.Message); } } if (exception.InnerException != null) { FlattenException(exception.InnerException, sentryEvent, key + ".innerException"); } }
public void Process_EventHasExtrasExceptionDoesnt_NotModified() { var evt = new SentryEvent(); evt.SetExtra("extra", 1); var expected = evt.Extra; var ex = new Exception(); Sut.Process(ex, evt); Assert.Same(expected, evt.Extra); }
public SentryEvent Process(SentryEvent @event) { if (serviceContext.TryGetInstance <ExplorerContext>() is ExplorerContext context) { @event.SetTag("ColumnType", context.ColumnInfo.Type.ToString()); @event.SetExtra("ExplorationContext", context); } @event.SetTag("GitSha", ThisAssembly.Git.Sha); @event.SetTag("GitBranch", ThisAssembly.Git.Branch); return(@event); }
public void Log(object logMessage, LogLevel logLevel, params string[] vals) { if (logLevel == LogLevel.Critical && logMessage is Exception exception && !(exception is NonLoggableException)) { var sentryEvent = new SentryEvent(exception); lock (_lockingObject) { foreach (var contextKeyValue in ContextData) { sentryEvent.SetExtra(contextKeyValue.Key, contextKeyValue.Value); } SentryClient.CaptureEvent(sentryEvent); } } }
private static SentryEvent SentryBeforeSend(SentryEvent args) { args.User = new User { Id = AssistantOptions.UserId }; args.SetTag("SessionId", AssistantOptions.SessionId); args.SetExtra("PlayerName", Engine.Player?.Name ?? "Unknown"); args.SetExtra("PlayerSerial", Engine.Player?.Serial ?? 0); args.SetExtra("Shard", Engine.CurrentShard?.Name ?? "Unknown"); args.SetExtra("ShardFeatures", Engine.Features.ToString()); args.SetExtra("CharacterListFlags", Engine.CharacterListFlags.ToString()); args.SetExtra("Connected", Engine.Connected); args.SetExtra("ClientVersion", Engine.ClientVersion == null ? "Unknown" : Engine.ClientVersion.ToString()); args.SetExtra("KeyboardLayout", InputLanguageManager.Current?.CurrentInputLanguage?.Name ?? "Unknown"); return(args); }
public void Process_EventAndExceptionHaveExtra_DataCombined() { const string expectedKey = "extra"; const int expectedValue = 1; var evt = new SentryEvent(); evt.SetExtra(expectedKey, expectedValue); var ex = new Exception(); ex.Data.Add("other extra", 2); Sut.Process(ex, evt); Assert.Equal(2, evt.Extra.Count); Assert.Contains(evt.Extra, p => p.Key == expectedKey && (int)p.Value == expectedValue); }
// SentryException.Extra is not supported by Sentry yet. // Move the extras to the Event Extra while marking // by index the Exception which owns it. private static void MoveExceptionExtrasToEvent( SentryEvent sentryEvent, IReadOnlyList <SentryException> sentryExceptions) { for (var i = 0; i < sentryExceptions.Count; i++) { var sentryException = sentryExceptions[i]; if (sentryException.Data.Count <= 0) { continue; } foreach (var key in sentryException.Data.Keys) { sentryEvent.SetExtra($"Exception[{i}][{key}]", sentryException.Data[key]); } } }
/// <summary> /// Extracts details from <see cref="DbEntityValidationException"/> into the <see cref="SentryEvent"/>. /// </summary> protected override void ProcessException(DbEntityValidationException exception, SentryEvent sentryEvent) { var errorList = new Dictionary <string, List <string> >(); foreach (var error in exception.EntityValidationErrors.SelectMany(x => x.ValidationErrors)) { if (errorList.TryGetValue(error.PropertyName, out var list)) { list.Add(error.ErrorMessage); } else { list = new List <string> { error.ErrorMessage }; errorList.Add(error.PropertyName, list); } } sentryEvent.SetExtra(EntityValidationErrors, errorList); }
public void Process(Exception exception, SentryEvent sentryEvent) { if ((bool)exception.Data["TickException"]) { return; } // create folder "crashes" var crashes = Path.Combine(AppContext.BaseDirectory, "Crashes"); Directory.CreateDirectory(crashes); // open filestream to write minidump to crashes folder var file = new FileStream(Path.Combine(crashes, $"{Guid.NewGuid()}.dmp"), FileMode.Create); var info = new MINIDUMP_EXCEPTION_INFORMATION { ThreadId = GetCurrentThreadId(), ExceptionPointers = Marshal.GetExceptionPointers(), ClientPointers = 1 }; MiniDumpWriteDump( GetCurrentProcess(), GetCurrentProcessId(), file.SafeFileHandle.DangerousGetHandle(), (int)MINIDUMP_TYPE.MiniDumpWithFullMemory, ref info, IntPtr.Zero, IntPtr.Zero); file.Close(); // inform the user a minidump has been written Console.ForegroundColor = ConsoleColor.Red; Console.WriteLine("A minidump has been written to " + file.Name + ", please send this file to the developers."); Console.ResetColor(); sentryEvent.SetExtra("minidump", Path.GetFileName(file.Name)); }
// SentryException.Extra is not supported by Sentry yet. // Move the extras to the Event Extra while marking // by index the Exception which owns it. private static void MoveExceptionExtrasToEvent( SentryEvent sentryEvent, IReadOnlyList <SentryException> sentryExceptions) { for (var i = 0; i < sentryExceptions.Count; i++) { var sentryException = sentryExceptions[i]; if (sentryException.Data.Count <= 0) { continue; } foreach (var keyValue in sentryException.Data) { if (keyValue.Key.StartsWith("sentry:", StringComparison.OrdinalIgnoreCase) && keyValue.Value != null) { if (keyValue.Key.StartsWith(ExceptionDataTagKey, StringComparison.OrdinalIgnoreCase) && keyValue.Value is string tagValue && ExceptionDataTagKey.Length < keyValue.Key.Length) { // Set the key after the ExceptionDataTagKey string. sentryEvent.SetTag(keyValue.Key.Substring(ExceptionDataTagKey.Length), tagValue); } else if (keyValue.Key.StartsWith(ExceptionDataContextKey, StringComparison.OrdinalIgnoreCase) && ExceptionDataContextKey.Length < keyValue.Key.Length) { // Set the key after the ExceptionDataTagKey string. _ = sentryEvent.Contexts[keyValue.Key.Substring(ExceptionDataContextKey.Length)] = keyValue.Value; } else { sentryEvent.SetExtra($"Exception[{i}][{keyValue.Key}]", sentryException.Data[keyValue.Key]); } }
public void SerializeObject_AllPropertiesSetToNonDefault_SerializesValidObject() { var ex = new Exception("exception message"); var timestamp = DateTimeOffset.MaxValue; var id = Guid.Parse("4b780f4c-ec03-42a7-8ef8-a41c9d5621f8"); var sut = new SentryEvent(ex, timestamp, id) { User = new User { Id = "user-id" }, Request = new Request { Method = "POST" }, Contexts = new Contexts { ["context_key"] = "context_value", [".NET Framework"] = new Dictionary <string, string> { [".NET Framework"] = "\"v2.0.50727\", \"v3.0\", \"v3.5\"", [".NET Framework Client"] = "\"v4.8\", \"v4.0.0.0\"", [".NET Framework Full"] = "\"v4.8\"" } }, Sdk = new SdkVersion { Name = "SDK-test", Version = "1.1.1" }, Environment = "environment", Level = SentryLevel.Fatal, Logger = "logger", Message = new SentryMessage { Message = "message", Formatted = "structured_message" }, Modules = { { "module_key", "module_value" } }, Release = "release", SentryExceptions = new[] { new SentryException { Value = "exception_value" } }, SentryThreads = new[] { new SentryThread { Crashed = true } }, ServerName = "server_name", TransactionName = "transaction", }; sut.Sdk.AddPackage(new Package("name", "version")); sut.AddBreadcrumb(new Breadcrumb(timestamp, "crumb")); sut.AddBreadcrumb(new Breadcrumb( timestamp, "message", "type", new Dictionary <string, string> { { "data-key", "data-value" } }, "category", BreadcrumbLevel.Warning)); sut.SetExtra("extra_key", "extra_value"); sut.Fingerprint = new[] { "fingerprint" }; sut.SetTag("tag_key", "tag_value"); var actualString = sut.ToJsonString(); var actual = SentryEvent.FromJson(Json.Parse(actualString)); // Assert actual.Should().BeEquivalentTo(sut, o => { // Exceptions are not deserialized o.Excluding(x => x.Exception); // Timestamps lose some precision when writing to JSON o.Using <DateTimeOffset>(ctx => ctx.Subject.Should().BeCloseTo(ctx.Expectation, TimeSpan.FromMilliseconds(1)) ).WhenTypeIs <DateTimeOffset>(); return(o); }); }
protected override void ProcessException(DBConcurrencyException exception, SentryEvent sentryEvent) { sentryEvent.SetExtra("Row Count", exception.RowCount); sentryEvent.SetExtra("Row Error", exception.Row.RowError); }
public SentryEvent?Process(SentryEvent @event) { if (@event is null) { return(null); } // Add some Unity specific context: var version = "0.0.1-alpha"; // TODO Sdk shouldn't be marked as nullable @event.Sdk !.AddPackage("github:sentry.unity", version); @event.Sdk.Name = "sentry.unity"; @event.Sdk.Version = version; @event.Contexts.OperatingSystem.Name = SystemInfo.operatingSystem; @event.Contexts.Device.Name = SystemInfo.deviceName; #pragma warning disable RECS0018 // Value is exact when expressing no battery level if (SystemInfo.batteryLevel != -1.0) #pragma warning restore RECS0018 { @event.Contexts.Device.BatteryLevel = (short?)(SystemInfo.batteryLevel * 100); } @event.Release = Application.version; // This is the approximate amount of system memory in megabytes. // This function is not supported on Windows Store Apps and will always return 0. @event.Contexts.Device.MemorySize = SystemInfo.systemMemorySize; @event.Contexts.Device.Timezone = TimeZoneInfo.Local; @event.Contexts.App.StartTime = DateTimeOffset.UtcNow.AddSeconds(-Time.realtimeSinceStartup); // TODO: @event.Contexts["Unity"] = new UnityContext (values to be read on the main thread) @event.SetExtra("unity:processorCount", SystemInfo.processorCount.ToString()); @event.SetExtra("unity:supportsVibration", SystemInfo.supportsVibration.ToString()); @event.SetExtra("unity:installMode", Application.installMode.ToString()); // TODO: Will move to raw_description once parsing is done in Sentry @event.Contexts.OperatingSystem.Name = SystemInfo.operatingSystem; switch (Input.deviceOrientation) { case UnityEngine.DeviceOrientation.Portrait: case UnityEngine.DeviceOrientation.PortraitUpsideDown: @event.Contexts.Device.Orientation = DeviceOrientation.Portrait; break; case UnityEngine.DeviceOrientation.LandscapeLeft: case UnityEngine.DeviceOrientation.LandscapeRight: @event.Contexts.Device.Orientation = DeviceOrientation.Landscape; break; case UnityEngine.DeviceOrientation.FaceUp: case UnityEngine.DeviceOrientation.FaceDown: // TODO: Add to protocol? break; } var model = SystemInfo.deviceModel; if (model != SystemInfo.unsupportedIdentifier // Returned by the editor && model != "System Product Name (System manufacturer)") { @event.Contexts.Device.Model = model; } //device.DeviceType = SystemInfo.deviceType.ToString(); //device.CpuDescription = SystemInfo.processorType; //device.BatteryStatus = SystemInfo.batteryStatus.ToString(); @event.SetExtra("unity:batteryStatus", SystemInfo.batteryStatus.ToString()); @event.SetExtra("unity:deviceType", SystemInfo.deviceType.ToString()); @event.SetExtra("unity:processorType", SystemInfo.processorType); // This is the approximate amount of system memory in megabytes. // This function is not supported on Windows Store Apps and will always return 0. if (SystemInfo.systemMemorySize != 0) { @event.Contexts.Device.MemorySize = SystemInfo.systemMemorySize * 1048576L; // Sentry device mem is in Bytes } @event.Contexts.Gpu.Id = SystemInfo.graphicsDeviceID; @event.Contexts.Gpu.Name = SystemInfo.graphicsDeviceName; @event.Contexts.Gpu.VendorId = SystemInfo.graphicsDeviceVendorID.ToString(); @event.Contexts.Gpu.VendorName = SystemInfo.graphicsDeviceVendor; @event.Contexts.Gpu.MemorySize = SystemInfo.graphicsMemorySize; @event.Contexts.Gpu.MultiThreadedRendering = SystemInfo.graphicsMultiThreaded; @event.Contexts.Gpu.NpotSupport = SystemInfo.npotSupport.ToString(); @event.Contexts.Gpu.Version = SystemInfo.graphicsDeviceVersion; @event.Contexts.Gpu.ApiType = SystemInfo.graphicsDeviceType.ToString(); @event.Contexts.App.StartTime = DateTimeOffset.UtcNow // NOTE: Time API requires main thread .AddSeconds(-Time.realtimeSinceStartup); if (Debug.isDebugBuild) { @event.Contexts.App.BuildType = "debug"; } else { @event.Contexts.App.BuildType = "release"; } // TODO: 'UNITY_EDITOR' preprocessor is not known from within 'Sentry.Unity' #if UNITY_EDITOR @event.Contexts.Device.Simulator = true; #else @event.Contexts.Device.Simulator = false; #endif return(@event); }
public void Log <TState>( LogLevel logLevel, EventId eventId, TState state, Exception exception, Func <TState, Exception, string> formatter) { if (!IsEnabled(logLevel)) { return; } var message = formatter?.Invoke(state, exception); if (_options.MinimumEventLevel != LogLevel.None && logLevel >= _options.MinimumEventLevel) { var @event = new SentryEvent(exception) { Logger = CategoryName, }; if (message != null) { // TODO: this will override the current message // which could have been set from reading Exception.Message if (@event.Message != null) { @event.SetExtra("original_message", @event.Message); } @event.Message = message; } var tuple = eventId.ToTupleOrNull(); if (tuple.HasValue) { @event.SetTag(tuple.Value.name, tuple.Value.value); } _sentryClient.CaptureEvent(@event); } // Even if it was sent as event, add breadcrumb so next event includes it if (_options.MinimumBreadcrumbLevel != LogLevel.None && logLevel >= _options.MinimumBreadcrumbLevel) { var data = eventId.ToDictionaryOrNull(); if (exception != null && message != null) { // Exception.Message won't be used as Breadcrumb message // Avoid losing it by adding as data: data = data ?? new Dictionary <string, string>(); data.Add("exception_message", exception.Message); } _sentryClient.AddBreadcrumb( _clock, message ?? exception?.Message, "default", CategoryName, data, logLevel.ToBreadcrumbLevel()); } }
public void SerializeObject_AllPropertiesSetToNonDefault_SerializesValidObject() { var ex = new Exception("exception message"); var timestamp = DateTimeOffset.MaxValue; var id = Guid.Parse("4b780f4c-ec03-42a7-8ef8-a41c9d5621f8"); var sut = new SentryEvent(ex, timestamp, id) { User = new User { Id = "user-id" }, Request = new Request { Method = "POST" }, Contexts = new Contexts { ["context_key"] = "context_value", [".NET Framework"] = new Dictionary <string, string> { [".NET Framework"] = "\"v2.0.50727\", \"v3.0\", \"v3.5\"", [".NET Framework Client"] = "\"v4.8\", \"v4.0.0.0\"", [".NET Framework Full"] = "\"v4.8\"" } }, Sdk = new SdkVersion { Name = "SDK-test", Version = "1.1.1" }, Environment = "environment", Level = SentryLevel.Fatal, Logger = "logger", Message = new SentryMessage { Message = "message", Formatted = "structured_message" }, Modules = { { "module_key", "module_value" } }, Release = "release", SentryExceptions = new[] { new SentryException { Value = "exception_value" } }, SentryThreads = new[] { new SentryThread { Crashed = true } }, ServerName = "server_name", TransactionName = "transaction", }; sut.Sdk.AddPackage(new Package("name", "version")); sut.AddBreadcrumb(new Breadcrumb(timestamp, "crumb")); sut.AddBreadcrumb(new Breadcrumb( timestamp, "message", "type", new Dictionary <string, string> { { "data-key", "data-value" } }, "category", BreadcrumbLevel.Warning)); sut.SetExtra("extra_key", "extra_value"); sut.Fingerprint = new[] { "fingerprint" }; sut.SetTag("tag_key", "tag_value"); var actualString = sut.ToJsonString(); var actual = SentryEvent.FromJson(Json.Parse(actualString)); actual.Should().BeEquivalentTo(sut, o => { // Due to timestamp precision o.Excluding(e => e.Breadcrumbs); o.Excluding(e => e.Exception); return(o); }); // Expected item[0].Timestamp to be <9999-12-31 23:59:59.9999999>, but found <9999-12-31 23:59:59.999>. actual.Breadcrumbs.Should().BeEquivalentTo(sut.Breadcrumbs, o => o.Excluding(b => b.Timestamp)); var counter = 0; foreach (var sutBreadcrumb in sut.Breadcrumbs) { sutBreadcrumb.Timestamp.Should().BeCloseTo(actual.Breadcrumbs.ElementAt(counter++).Timestamp); } }
public async Task Roundtrip_WithEvent_Success() { // Arrange var ex = new Exception("exception message"); var timestamp = DateTimeOffset.MaxValue; var id = Guid.Parse("4b780f4c-ec03-42a7-8ef8-a41c9d5621f8"); var @event = new SentryEvent(ex, timestamp, id) { User = new User { Id = "user-id" }, Request = new Request { Method = "POST" }, Contexts = new Contexts { ["context_key"] = "context_value" }, Sdk = new SdkVersion { Name = "SDK-test", Version = "1.0.0" }, Environment = "environment", Level = SentryLevel.Fatal, Logger = "logger", Message = new SentryMessage { Message = "message", Formatted = "structured_message" }, Modules = { { "module_key", "module_value" } }, Release = "release", SentryExceptions = new[] { new SentryException { Value = "exception_value" } }, SentryThreads = new[] { new SentryThread { Crashed = true } }, ServerName = "server_name", TransactionName = "transaction", }; @event.SetExtra("extra_key", "extra_value"); @event.Fingerprint = new[] { "fingerprint" }; @event.SetTag("tag_key", "tag_value"); using var envelope = Envelope.FromEvent(@event); #if !NET461 && !NETCOREAPP2_1 await #endif using var stream = new MemoryStream(); // Act await envelope.SerializeAsync(stream); stream.Seek(0, SeekOrigin.Begin); using var envelopeRoundtrip = await Envelope.DeserializeAsync(stream); // Assert // Can't compare the entire object graph because output envelope contains evaluated length, // which original envelope doesn't have. envelopeRoundtrip.Header.Should().BeEquivalentTo(envelope.Header); envelopeRoundtrip.Items.Should().ContainSingle(); var payloadContent = (envelopeRoundtrip.Items[0].Payload as JsonSerializable)?.Source; payloadContent.Should().BeEquivalentTo(@event, o => o.Excluding(x => x.Exception)); }