From 52c9aff1079fec650693b773ab684803803632b2 Mon Sep 17 00:00:00 2001 From: Virgile Bello Date: Fri, 7 Aug 2026 14:50:38 +0200 Subject: [PATCH 01/12] Remove duplicate Stride.Editor.CrashReport project reference --- sources/launcher/Stride.Launcher/Stride.Launcher.csproj | 1 - 1 file changed, 1 deletion(-) diff --git a/sources/launcher/Stride.Launcher/Stride.Launcher.csproj b/sources/launcher/Stride.Launcher/Stride.Launcher.csproj index fa3d8aad76..d939250019 100644 --- a/sources/launcher/Stride.Launcher/Stride.Launcher.csproj +++ b/sources/launcher/Stride.Launcher/Stride.Launcher.csproj @@ -104,7 +104,6 @@ - From 48568147f9b7d7d070e03b7e38d4b811bb2eec15 Mon Sep 17 00:00:00 2001 From: Virgile Bello Date: Fri, 7 Aug 2026 14:50:38 +0200 Subject: [PATCH 02/12] Crash report: optional upload to Sentry Official builds bake a DSN via StrideSentryDsn (false disables sending, unset lets the user pick a destination per crash). Reports carry app@version release, environment tag, and are anonymized including Sentry stack frames. Nothing is sent without a click. --- sources/Directory.Packages.props | 1 + .../CrashReportAnonymizer.cs | 38 ++++++++ .../CrashReportSender.cs | 91 +++++++++++++++++++ .../CrashReportWindow.xaml | 24 ++++- .../CrashReportWindow.xaml.cs | 69 +++++++++++++- .../Stride.Editor.CrashReport.csproj | 14 +++ .../Helpers/CrashReportHelper.cs | 19 +--- sources/editor/Stride.GameStudio/Program.cs | 3 +- .../CrashReport/CrashReportHelper.cs | 10 +- 9 files changed, 244 insertions(+), 25 deletions(-) create mode 100644 sources/editor/Stride.Editor.CrashReport/CrashReportAnonymizer.cs create mode 100644 sources/editor/Stride.Editor.CrashReport/CrashReportSender.cs diff --git a/sources/Directory.Packages.props b/sources/Directory.Packages.props index 75b26db028..1612cb4790 100644 --- a/sources/Directory.Packages.props +++ b/sources/Directory.Packages.props @@ -85,6 +85,7 @@ + diff --git a/sources/editor/Stride.Editor.CrashReport/CrashReportAnonymizer.cs b/sources/editor/Stride.Editor.CrashReport/CrashReportAnonymizer.cs new file mode 100644 index 0000000000..d1e7dde9f8 --- /dev/null +++ b/sources/editor/Stride.Editor.CrashReport/CrashReportAnonymizer.cs @@ -0,0 +1,38 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using System; +using System.Text.RegularExpressions; + +namespace Stride.Editor.CrashReport; + +/// +/// Strips the user name and profile path from crash report text before it leaves the machine. +/// It also makes paths easier to copy and paste between machines. +/// +public static class CrashReportAnonymizer +{ + public static void Scrub(CrashReportData report) + { + for (var i = 0; i < report.Data.Count; i++) + { + report.Data[i] = (report.Data[i].Item1, Scrub(report.Data[i].Item2)); + } + } + + public static string Scrub(string text) + { + if (string.IsNullOrEmpty(text)) + return text; + + var userProfile = Environment.GetEnvironmentVariable("USERPROFILE"); + if (!string.IsNullOrEmpty(userProfile)) + text = Regex.Replace(text, Regex.Escape(userProfile), "%USERPROFILE%", RegexOptions.IgnoreCase); + + var userName = Environment.GetEnvironmentVariable("USERNAME"); + if (!string.IsNullOrEmpty(userName)) + text = Regex.Replace(text, $@"\b{Regex.Escape(userName)}\b", "%USERNAME%", RegexOptions.IgnoreCase); + + return text; + } +} diff --git a/sources/editor/Stride.Editor.CrashReport/CrashReportSender.cs b/sources/editor/Stride.Editor.CrashReport/CrashReportSender.cs new file mode 100644 index 0000000000..61ba761afc --- /dev/null +++ b/sources/editor/Stride.Editor.CrashReport/CrashReportSender.cs @@ -0,0 +1,91 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using System; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; +using Sentry; + +namespace Stride.Editor.CrashReport; + +/// +/// Sends crash reports to Sentry. Official builds bake their destination in through the StrideSentryDsn +/// property; source builds have no destination and let the user pick one per crash. +/// +public static class CrashReportSender +{ + /// + /// Sentry project collecting reports from source builds. Offered as an explicit choice in the crash + /// window, never used silently. Empty until the Sentry project exists. + /// + public const string DevChannelDsn = ""; + + /// DSN baked in at build time, if any. + public static string BuildDsn { get; } = GetMetadata("SentryDsn"); + + /// True when the build opted out of crash sending entirely (StrideSentryDsn=false). + public static bool IsDisabled { get; } = GetMetadata("SentryDisabled") == "true"; + + public static async Task SendAsync(CrashReportData report, string applicationName, Exception exception, string dsn) + { + var version = Assembly.GetEntryAssembly()?.GetCustomAttribute()?.InformationalVersion ?? "unknown"; + var package = applicationName.Replace(" ", "").ToLowerInvariant(); + + using var sdk = SentrySdk.Init(options => + { + options.Dsn = dsn; + options.Release = $"{package}@{version}"; + options.Environment = GetMetadata("SentryEnvironment") ?? "local"; + options.IsGlobalModeEnabled = true; + options.AutoSessionTracking = false; + options.SetBeforeSend((sentryEvent, _) => Anonymize(sentryEvent)); + }); + + SentrySdk.ConfigureScope(scope => + { + scope.AddAttachment(Encoding.UTF8.GetBytes(report.ToString()), "report.txt"); + scope.SetTag("application", applicationName); + }); + + var sentryEvent = exception != null + ? new SentryEvent(exception) + : new SentryEvent { Message = new SentryMessage { Formatted = report["Exception"] } }; + sentryEvent.Level = SentryLevel.Fatal; + + SentrySdk.CaptureEvent(sentryEvent); + await SentrySdk.FlushAsync(TimeSpan.FromSeconds(15)); + } + + /// + /// The Sentry event carries its own copy of messages and stack frames, so it needs the same scrubbing + /// as the report text. + /// + private static SentryEvent Anonymize(SentryEvent sentryEvent) + { + if (sentryEvent.Message != null) + { + sentryEvent.Message.Formatted = CrashReportAnonymizer.Scrub(sentryEvent.Message.Formatted); + sentryEvent.Message.Message = CrashReportAnonymizer.Scrub(sentryEvent.Message.Message); + } + + foreach (var exception in sentryEvent.SentryExceptions ?? []) + { + exception.Value = CrashReportAnonymizer.Scrub(exception.Value); + foreach (var frame in exception.Stacktrace?.Frames ?? []) + { + frame.FileName = CrashReportAnonymizer.Scrub(frame.FileName); + frame.AbsolutePath = CrashReportAnonymizer.Scrub(frame.AbsolutePath); + } + } + + return sentryEvent; + } + + private static string GetMetadata(string key) + { + return typeof(CrashReportSender).Assembly.GetCustomAttributes() + .FirstOrDefault(x => x.Key == key)?.Value; + } +} diff --git a/sources/editor/Stride.Editor.CrashReport/CrashReportWindow.xaml b/sources/editor/Stride.Editor.CrashReport/CrashReportWindow.xaml index 7b3a9a2852..d3b230ccdc 100644 --- a/sources/editor/Stride.Editor.CrashReport/CrashReportWindow.xaml +++ b/sources/editor/Stride.Editor.CrashReport/CrashReportWindow.xaml @@ -4,7 +4,7 @@ Height="380" Width="580" Topmost="True" SizeToContent="Height" - MaxHeight="380" + MaxHeight="460" Background="{DynamicResource BackgroundBrush}" Title="Report your crash" ResizeMode="NoResize" @@ -117,7 +117,7 @@ Unfortunately, has crashed. - Please help us improve Stride by sending information about this crash through Github Issues. + Please help us improve Stride by sending this report, or by reporting the crash through Github Issues. + +