diff --git a/CHANGELOG.md b/CHANGELOG.md index e21476506e..a52995b6a7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ - Continue with adding MAUI support ([#1670](https://github.com/getsentry/sentry-dotnet/pull/1670)) - MAUI events become extra context in Sentry events ([#1706](https://github.com/getsentry/sentry-dotnet/pull/1706)) - Add options for PII breadcrumbs from MAUI events ([#1709](https://github.com/getsentry/sentry-dotnet/pull/1709)) + - Add device information to the event context ([#1713](https://github.com/getsentry/sentry-dotnet/pull/1713)) - Added a new `net6.0-android` target for the `Sentry` core library, which bundles the [Sentry Android SDK](https://docs.sentry.io/platforms/android/): - Initial .NET 6 Android support ([#1288](https://github.com/getsentry/sentry-dotnet/pull/1288)) - Update Android Support ([#1669](https://github.com/getsentry/sentry-dotnet/pull/1669)) diff --git a/samples/Sentry.Samples.Maui/MauiProgram.cs b/samples/Sentry.Samples.Maui/MauiProgram.cs index a885ec832c..7d1d764c17 100644 --- a/samples/Sentry.Samples.Maui/MauiProgram.cs +++ b/samples/Sentry.Samples.Maui/MauiProgram.cs @@ -9,7 +9,7 @@ public static MauiApp CreateMauiApp() => { options.Dsn = "https://eb18e953812b41c3aeb042e666fd3b5c@o447951.ingest.sentry.io/5428537"; options.Debug = true; - options.MaxBreadcrumbs = int.MaxValue; // TODO: reduce breadcrumbs, remove this + options.MaxBreadcrumbs = 1000; // TODO: reduce breadcrumbs, remove this }) .ConfigureFonts(fonts => { diff --git a/src/Sentry.Maui/Internal/MauiDeviceData.cs b/src/Sentry.Maui/Internal/MauiDeviceData.cs new file mode 100644 index 0000000000..7a25f6a8ed --- /dev/null +++ b/src/Sentry.Maui/Internal/MauiDeviceData.cs @@ -0,0 +1,105 @@ +using Sentry.Extensibility; +using Sentry.Protocol; +using Device = Sentry.Protocol.Device; + +namespace Sentry.Maui.Internal; + +internal static class MauiDeviceData +{ + public static void ApplyMauiDeviceData(this Device device, IDiagnosticLogger? logger) + { + try + { + // TODO: Add more device data where indicated + + // https://docs.microsoft.com/dotnet/maui/platform-integration/device/information + var deviceInfo = DeviceInfo.Current; + if (deviceInfo.Platform == DevicePlatform.Unknown) + { + // return early so we don't get NotImplementedExceptions (i.e., in unit tests, etc.) + return; + } + device.Name ??= deviceInfo.Name; + device.Manufacturer ??= deviceInfo.Manufacturer; + device.Model ??= deviceInfo.Model; + device.DeviceType ??= deviceInfo.Idiom.ToString(); + device.Simulator ??= deviceInfo.DeviceType switch + { + DeviceType.Virtual => true, + DeviceType.Physical => false, + _ => null + }; + // device.Brand ??= ? + // device.Family ??= ? + // device.ModelId ??= ? + // device.Architecture ??= ? + // ? = deviceInfo.Platform; + // ? = deviceInfo.VersionString; + + // https://docs.microsoft.com/dotnet/maui/platform-integration/device/battery + var battery = Battery.Default; + device.BatteryLevel ??= battery.ChargeLevel < 0 ? null : (short)battery.ChargeLevel; + device.BatteryStatus ??= battery.State.ToString(); + device.IsCharging ??= battery.State switch + { + BatteryState.Unknown => null, + BatteryState.Charging => true, + _ => false + }; + + // https://docs.microsoft.com/dotnet/maui/platform-integration/communication/networking#using-connectivity + var connectivity = Connectivity.Current; + device.IsOnline ??= connectivity.NetworkAccess == NetworkAccess.Internet; + + // https://docs.microsoft.com/dotnet/maui/platform-integration/device/display + var display = DeviceDisplay.Current.MainDisplayInfo; + device.ScreenResolution ??= $"{(int)display.Width}x{(int)display.Height}"; + device.ScreenDensity ??= (float)display.Density; + device.Orientation ??= display.Orientation switch + { + DisplayOrientation.Portrait => DeviceOrientation.Portrait, + DisplayOrientation.Landscape => DeviceOrientation.Landscape, + _ => null + }; + // device.ScreenDpi ??= ? + // ? = display.RefreshRate; + // ? = display.Rotation; + + // https://docs.microsoft.com/dotnet/maui/platform-integration/device/vibrate + device.SupportsVibration ??= Vibration.Default.IsSupported; + + // https://docs.microsoft.com/dotnet/maui/platform-integration/device/sensors + device.SupportsAccelerometer ??= Accelerometer.IsSupported; + device.SupportsGyroscope ??= Gyroscope.IsSupported; + + // https://docs.microsoft.com/dotnet/maui/platform-integration/device/geolocation + // TODO: How to get without actually trying to make a location request? + // device.SupportsLocationService ??= Geolocation.Default.??? + + // device.SupportsAudio ??= ? + + // device.MemorySize ??= + // device.FreeMemory ??= + // device.UsableMemory ??= + // device.LowMemory ??= + + // device.StorageSize ??= + // device.FreeStorage ??= + // device.ExternalStorageSize ??= + // device.ExternalFreeStorage ??= + + // device.BootTime ??= + // device.DeviceUniqueIdentifier ??= + + //device.CpuDescription ??= ? + //device.ProcessorCount ??= ? + //device.ProcessorFrequency ??= ? + + } + catch (Exception ex) + { + // Log, but swallow the exception so we can continue sending events + logger?.LogError("Error getting MAUI device information.", ex); + } + } +} diff --git a/src/Sentry.Maui/Internal/SentryMauiEventProcessor.cs b/src/Sentry.Maui/Internal/SentryMauiEventProcessor.cs index 973ddde7ac..909d90772f 100644 --- a/src/Sentry.Maui/Internal/SentryMauiEventProcessor.cs +++ b/src/Sentry.Maui/Internal/SentryMauiEventProcessor.cs @@ -4,11 +4,18 @@ namespace Sentry.Maui.Internal; internal class SentryMauiEventProcessor : ISentryEventProcessor { + private readonly SentryMauiOptions _options; + + public SentryMauiEventProcessor(SentryMauiOptions options) + { + _options = options; + } + public SentryEvent Process(SentryEvent @event) { - // Set SDK name and version for MAUI @event.Sdk.Name = Constants.SdkName; @event.Sdk.Version = Constants.SdkVersion; + @event.Contexts.Device.ApplyMauiDeviceData(_options.DiagnosticLogger); return @event; } diff --git a/src/Sentry.Maui/Internal/SentryMauiOptionsSetup.cs b/src/Sentry.Maui/Internal/SentryMauiOptionsSetup.cs index da424b19f2..4bec89bb30 100644 --- a/src/Sentry.Maui/Internal/SentryMauiOptionsSetup.cs +++ b/src/Sentry.Maui/Internal/SentryMauiOptionsSetup.cs @@ -20,6 +20,6 @@ public override void Configure(SentryMauiOptions options) options.IsGlobalModeEnabled = true; // We'll use an event processor to set things like SDK name - options.AddEventProcessor(new SentryMauiEventProcessor()); + options.AddEventProcessor(new SentryMauiEventProcessor(options)); } } diff --git a/src/Sentry/Android/AndroidEventProcessor.cs b/src/Sentry/Android/AndroidEventProcessor.cs new file mode 100644 index 0000000000..c5c17b9deb --- /dev/null +++ b/src/Sentry/Android/AndroidEventProcessor.cs @@ -0,0 +1,56 @@ +using Sentry.Android.Extensions; +using Sentry.Extensibility; + +namespace Sentry.Android; + +internal class AndroidEventProcessor : ISentryEventProcessor, IDisposable +{ + private readonly Java.IEventProcessor? _androidProcessor; + private readonly Java.Hint _hint = new(); + + public AndroidEventProcessor(SentryAndroidOptions androidOptions) + { + // Locate the Android SDK's default event processor by its class + // NOTE: This approach avoids hardcoding the class name (which could be obfuscated by proguard) + _androidProcessor = androidOptions.EventProcessors.OfType() + .Where(o => o.Class == JavaClass.FromType(typeof(DefaultAndroidEventProcessor))) + .Cast() + .FirstOrDefault(); + + // TODO: This would be cleaner, but doesn't compile. Figure out why. + // _androidProcessor = androidOptions.EventProcessors + // .OfType() + // .FirstOrDefault(); + } + + public SentryEvent Process(SentryEvent @event) + { + // Get what information we can ourselves first + @event.Contexts.Device.ApplyFromAndroidRuntime(); + + // Copy more information from the Android SDK + if (_androidProcessor is { } androidProcessor) + { + // TODO: Can we gather more device data directly and remove this? + + // Run a fake event through the Android processor, so we can get context info from the Android SDK. + // We'll want to do this every time, so that all information is current. (ex: device orientation) + using var e = new Java.SentryEvent(); + androidProcessor.Process(e, _hint); + + // Copy what we need to the managed event + if (e.Contexts.Device is { } device) + { + @event.Contexts.Device.ApplyFromSentryAndroidSdk(device); + } + } + + return @event; + } + + public void Dispose() + { + _androidProcessor?.Dispose(); + _hint.Dispose(); + } +} diff --git a/src/Sentry/Android/Extensions/DeviceExtensions.cs b/src/Sentry/Android/Extensions/DeviceExtensions.cs new file mode 100644 index 0000000000..48a231c07e --- /dev/null +++ b/src/Sentry/Android/Extensions/DeviceExtensions.cs @@ -0,0 +1,76 @@ +using Sentry.Protocol; + +namespace Sentry.Android.Extensions; + +internal static class DeviceExtensions +{ + public static void ApplyFromAndroidRuntime(this Device device) + { + device.Manufacturer ??= AndroidBuild.Manufacturer; + device.Brand ??= AndroidBuild.Brand; + device.Model ??= AndroidBuild.Model; + + if (AndroidBuild.SupportedAbis is { } abis) + { + device.Architecture ??= abis[0]; + } + else + { +#pragma warning disable CS0618 // Type or member is obsolete + device.Architecture ??= AndroidBuild.CpuAbi; +#pragma warning restore CS0618 // Type or member is obsolete + } + } + + public static void ApplyFromSentryAndroidSdk(this Device device, Java.Protocol.Device d) + { + // We already have these above + // device.Manufacturer ??= d.Manufacturer; + // device.Brand ??= d.Brand; + // device.Model ??= d.Model; + // device.Architecture ??= d.GetArchs()?.FirstOrDefault(); + + device.Name ??= d.Name; + device.Family ??= d.Family; + device.ModelId ??= d.ModelId; + device.BatteryLevel ??= d.BatteryLevel?.ShortValue(); + device.IsCharging ??= d.IsCharging()?.BooleanValue(); + device.IsOnline ??= d.IsOnline()?.BooleanValue(); + device.Orientation ??= d.Orientation?.ToDeviceOrientation(); + device.Simulator ??= d.IsSimulator()?.BooleanValue(); + device.MemorySize ??= d.MemorySize?.LongValue(); + device.FreeMemory ??= d.FreeMemory?.LongValue(); + device.UsableMemory ??= d.UsableMemory?.LongValue(); + device.LowMemory ??= d.IsLowMemory()?.BooleanValue(); + device.StorageSize ??= d.StorageSize?.LongValue(); + device.FreeStorage ??= d.FreeStorage?.LongValue(); + device.ExternalStorageSize ??= d.ExternalStorageSize?.LongValue(); + device.ExternalFreeStorage ??= d.ExternalFreeStorage?.LongValue(); + device.ScreenResolution ??= $"{d.ScreenWidthPixels}x{d.ScreenHeightPixels}"; + device.ScreenDensity ??= d.ScreenDensity?.FloatValue(); + device.ScreenDpi ??= d.ScreenDpi?.IntValue(); + device.BootTime ??= d.BootTime?.ToDateTimeOffset(); + device.DeviceUniqueIdentifier ??= d.Id; + + // TODO: Can we get these from somewhere? + //device.ProcessorCount ??= ? + //device.CpuDescription ??= ? + //device.ProcessorFrequency ??= ? + //device.DeviceType ??= ? + //device.BatteryStatus ??= ? + //device.SupportsVibration ??= ? + //device.SupportsAccelerometer ??= ? + //device.SupportsGyroscope ??= ? + //device.SupportsAudio ??= ? + //device.SupportsLocationService ??= ? + + } + + public static DeviceOrientation ToDeviceOrientation(this Java.Protocol.Device.DeviceOrientation orientation) => + orientation.Name() switch + { + "PORTRAIT" => DeviceOrientation.Portrait, + "LANDSCAPE" => DeviceOrientation.Landscape, + _ => throw new ArgumentOutOfRangeException(nameof(orientation), orientation.Name(), message: default) + }; +} diff --git a/src/Sentry/Android/SentrySdk.cs b/src/Sentry/Android/SentrySdk.cs index b34f2e2526..519de58b4f 100644 --- a/src/Sentry/Android/SentrySdk.cs +++ b/src/Sentry/Android/SentrySdk.cs @@ -29,30 +29,14 @@ public static IDisposable Init(AndroidContext context, Action? co /// An object that should be disposed when the application terminates. public static IDisposable Init(AndroidContext context, SentryOptions options) { - // TODO: Pause/Resume - options.AutoSessionTracking = true; - options.IsGlobalModeEnabled = true; - options.AddEventProcessor(new DelegateEventProcessor(evt => - { - if (AndroidBuild.SupportedAbis is { } abis) - { - evt.Contexts.Device.Architecture = abis[0]; - } - else - { -#pragma warning disable CS0618 // Type or member is obsolete - evt.Contexts.Device.Architecture = AndroidBuild.CpuAbi; -#pragma warning restore CS0618 // Type or member is obsolete - } - - evt.Contexts.Device.Manufacturer = AndroidBuild.Manufacturer; - - return evt; - })); - + // Init the Java Android SDK first + SentryAndroidOptions? androidOptions = null; SentryAndroid.Init(context, new JavaLogger(options), new OptionsConfigurationCallback(o => { + // Capture the android options reference on the outer scope + androidOptions = o; + // TODO: Should we set the DistinctId to match the one used by GlobalSessionManager? //o.DistinctId = ? @@ -168,10 +152,17 @@ public static IDisposable Init(AndroidContext context, SentryOptions options) o.AddIgnoredExceptionForType(JavaClass.ForName("android.runtime.JavaProxyThrowable")); })); - options.CrashedLastRun = () => Java.Sentry.IsCrashedLastRun()?.BooleanValue() is true; - + // Make sure we capture managed exceptions from the Android environment AndroidEnvironment.UnhandledExceptionRaiser += AndroidEnvironment_UnhandledExceptionRaiser; + // Set options for the managed SDK + options.AutoSessionTracking = true; + options.IsGlobalModeEnabled = true; + options.AddEventProcessor(new AndroidEventProcessor(androidOptions!)); + options.CrashedLastRun = () => Java.Sentry.IsCrashedLastRun()?.BooleanValue() is true; + // TODO: Pause/Resume + + // Init the managed SDK return Init(options); } diff --git a/src/Sentry/Android/Transforms/Metadata.xml b/src/Sentry/Android/Transforms/Metadata.xml index 9c6bc0662a..37056dec78 100644 --- a/src/Sentry/Android/Transforms/Metadata.xml +++ b/src/Sentry/Android/Transforms/Metadata.xml @@ -46,6 +46,9 @@ SentryTraceHeaderName TraceStateHeaderName + + internal +