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

Add device information for MAUI and Android #1713

Merged
merged 9 commits into from
Jun 15, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
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?
mattjohnsonpint marked this conversation as resolved.
Show resolved Hide resolved
/// See https://github.com/getsentry/sentry-java/pull/1455
public string? DeviceUniqueIdentifier { get; set; }

/// <summary>
Expand Down