Skip to content

Commit

Permalink
Add device information for MAUI and Android (#1713)
Browse files Browse the repository at this point in the history
  • Loading branch information
mattjohnsonpint authored Jun 15, 2022
1 parent 16111ce commit a2db32b
Show file tree
Hide file tree
Showing 10 changed files with 268 additions and 27 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
2 changes: 1 addition & 1 deletion samples/Sentry.Samples.Maui/MauiProgram.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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 =>
{
Expand Down
105 changes: 105 additions & 0 deletions src/Sentry.Maui/Internal/MauiDeviceData.cs
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
9 changes: 8 additions & 1 deletion src/Sentry.Maui/Internal/SentryMauiEventProcessor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
2 changes: 1 addition & 1 deletion src/Sentry.Maui/Internal/SentryMauiOptionsSetup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}
}
56 changes: 56 additions & 0 deletions src/Sentry/Android/AndroidEventProcessor.cs
Original file line number Diff line number Diff line change
@@ -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<JavaObject>()
.Where(o => o.Class == JavaClass.FromType(typeof(DefaultAndroidEventProcessor)))
.Cast<Java.IEventProcessor>()
.FirstOrDefault();

// TODO: This would be cleaner, but doesn't compile. Figure out why.
// _androidProcessor = androidOptions.EventProcessors
// .OfType<DefaultAndroidEventProcessor>()
// .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();
}
}
76 changes: 76 additions & 0 deletions src/Sentry/Android/Extensions/DeviceExtensions.cs
Original file line number Diff line number Diff line change
@@ -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)
};
}
37 changes: 14 additions & 23 deletions src/Sentry/Android/SentrySdk.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,30 +29,14 @@ public static IDisposable Init(AndroidContext context, Action<SentryOptions>? co
/// <returns>An object that should be disposed when the application terminates.</returns>
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 = ?
Expand Down Expand Up @@ -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);
}

Expand Down
3 changes: 3 additions & 0 deletions src/Sentry/Android/Transforms/Metadata.xml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@
<attr path="/api/package[@name='io.sentry']/class[@name='SentryTraceHeader']/field[@name='SENTRY_TRACE_HEADER']" name="managedName">SentryTraceHeaderName</attr>
<attr path="/api/package[@name='io.sentry']/class[@name='TraceStateHeader']/field[@name='TRACE_STATE_HEADER']" name="managedName">TraceStateHeaderName</attr>

<!-- Fix visibility of this type, for use in AndroidEventProcessor.cs -->
<attr path="/api/package[@name='io.sentry.android.core']/class[@name='DefaultAndroidEventProcessor']" name="visibility">internal</attr>

<!--
The remaining APIS are removed to prevent various errors/warnings.
TODO: Find other workarounds for each one, rather than removing the APIs.
Expand Down
4 changes: 3 additions & 1 deletion src/Sentry/Protocol/Device.cs
Original file line number Diff line number Diff line change
Expand Up @@ -209,10 +209,12 @@ public sealed class Device : IJsonSerializable
/// </summary>
/// <example>
/// iOS: UIDevice.identifierForVendor (UUID)
/// Android: md5 of ANDROID_ID
/// Android: The generated Installation ID
/// Windows Store Apps: AdvertisingManager::AdvertisingId (possible fallback to HardwareIdentification::GetPackageSpecificToken().Id)
/// Windows Standalone: hash from the concatenation of strings taken from Computer System Hardware Classes
/// </example>
/// TODO: Investigate - Do ALL platforms now return a generated installation ID?
/// See https://github.com/getsentry/sentry-java/pull/1455
public string? DeviceUniqueIdentifier { get; set; }

/// <summary>
Expand Down

0 comments on commit a2db32b

Please sign in to comment.