Skip to content

Commit a2db32b

Browse files
Add device information for MAUI and Android (#1713)
1 parent 16111ce commit a2db32b

File tree

10 files changed

+268
-27
lines changed

10 files changed

+268
-27
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
- Continue with adding MAUI support ([#1670](https://github.com/getsentry/sentry-dotnet/pull/1670))
2323
- MAUI events become extra context in Sentry events ([#1706](https://github.com/getsentry/sentry-dotnet/pull/1706))
2424
- Add options for PII breadcrumbs from MAUI events ([#1709](https://github.com/getsentry/sentry-dotnet/pull/1709))
25+
- Add device information to the event context ([#1713](https://github.com/getsentry/sentry-dotnet/pull/1713))
2526
- Added a new `net6.0-android` target for the `Sentry` core library, which bundles the [Sentry Android SDK](https://docs.sentry.io/platforms/android/):
2627
- Initial .NET 6 Android support ([#1288](https://github.com/getsentry/sentry-dotnet/pull/1288))
2728
- Update Android Support ([#1669](https://github.com/getsentry/sentry-dotnet/pull/1669))

samples/Sentry.Samples.Maui/MauiProgram.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ public static MauiApp CreateMauiApp() =>
99
{
1010
options.Dsn = "https://[email protected]/5428537";
1111
options.Debug = true;
12-
options.MaxBreadcrumbs = int.MaxValue; // TODO: reduce breadcrumbs, remove this
12+
options.MaxBreadcrumbs = 1000; // TODO: reduce breadcrumbs, remove this
1313
})
1414
.ConfigureFonts(fonts =>
1515
{
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
using Sentry.Extensibility;
2+
using Sentry.Protocol;
3+
using Device = Sentry.Protocol.Device;
4+
5+
namespace Sentry.Maui.Internal;
6+
7+
internal static class MauiDeviceData
8+
{
9+
public static void ApplyMauiDeviceData(this Device device, IDiagnosticLogger? logger)
10+
{
11+
try
12+
{
13+
// TODO: Add more device data where indicated
14+
15+
// https://docs.microsoft.com/dotnet/maui/platform-integration/device/information
16+
var deviceInfo = DeviceInfo.Current;
17+
if (deviceInfo.Platform == DevicePlatform.Unknown)
18+
{
19+
// return early so we don't get NotImplementedExceptions (i.e., in unit tests, etc.)
20+
return;
21+
}
22+
device.Name ??= deviceInfo.Name;
23+
device.Manufacturer ??= deviceInfo.Manufacturer;
24+
device.Model ??= deviceInfo.Model;
25+
device.DeviceType ??= deviceInfo.Idiom.ToString();
26+
device.Simulator ??= deviceInfo.DeviceType switch
27+
{
28+
DeviceType.Virtual => true,
29+
DeviceType.Physical => false,
30+
_ => null
31+
};
32+
// device.Brand ??= ?
33+
// device.Family ??= ?
34+
// device.ModelId ??= ?
35+
// device.Architecture ??= ?
36+
// ? = deviceInfo.Platform;
37+
// ? = deviceInfo.VersionString;
38+
39+
// https://docs.microsoft.com/dotnet/maui/platform-integration/device/battery
40+
var battery = Battery.Default;
41+
device.BatteryLevel ??= battery.ChargeLevel < 0 ? null : (short)battery.ChargeLevel;
42+
device.BatteryStatus ??= battery.State.ToString();
43+
device.IsCharging ??= battery.State switch
44+
{
45+
BatteryState.Unknown => null,
46+
BatteryState.Charging => true,
47+
_ => false
48+
};
49+
50+
// https://docs.microsoft.com/dotnet/maui/platform-integration/communication/networking#using-connectivity
51+
var connectivity = Connectivity.Current;
52+
device.IsOnline ??= connectivity.NetworkAccess == NetworkAccess.Internet;
53+
54+
// https://docs.microsoft.com/dotnet/maui/platform-integration/device/display
55+
var display = DeviceDisplay.Current.MainDisplayInfo;
56+
device.ScreenResolution ??= $"{(int)display.Width}x{(int)display.Height}";
57+
device.ScreenDensity ??= (float)display.Density;
58+
device.Orientation ??= display.Orientation switch
59+
{
60+
DisplayOrientation.Portrait => DeviceOrientation.Portrait,
61+
DisplayOrientation.Landscape => DeviceOrientation.Landscape,
62+
_ => null
63+
};
64+
// device.ScreenDpi ??= ?
65+
// ? = display.RefreshRate;
66+
// ? = display.Rotation;
67+
68+
// https://docs.microsoft.com/dotnet/maui/platform-integration/device/vibrate
69+
device.SupportsVibration ??= Vibration.Default.IsSupported;
70+
71+
// https://docs.microsoft.com/dotnet/maui/platform-integration/device/sensors
72+
device.SupportsAccelerometer ??= Accelerometer.IsSupported;
73+
device.SupportsGyroscope ??= Gyroscope.IsSupported;
74+
75+
// https://docs.microsoft.com/dotnet/maui/platform-integration/device/geolocation
76+
// TODO: How to get without actually trying to make a location request?
77+
// device.SupportsLocationService ??= Geolocation.Default.???
78+
79+
// device.SupportsAudio ??= ?
80+
81+
// device.MemorySize ??=
82+
// device.FreeMemory ??=
83+
// device.UsableMemory ??=
84+
// device.LowMemory ??=
85+
86+
// device.StorageSize ??=
87+
// device.FreeStorage ??=
88+
// device.ExternalStorageSize ??=
89+
// device.ExternalFreeStorage ??=
90+
91+
// device.BootTime ??=
92+
// device.DeviceUniqueIdentifier ??=
93+
94+
//device.CpuDescription ??= ?
95+
//device.ProcessorCount ??= ?
96+
//device.ProcessorFrequency ??= ?
97+
98+
}
99+
catch (Exception ex)
100+
{
101+
// Log, but swallow the exception so we can continue sending events
102+
logger?.LogError("Error getting MAUI device information.", ex);
103+
}
104+
}
105+
}

src/Sentry.Maui/Internal/SentryMauiEventProcessor.cs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,18 @@ namespace Sentry.Maui.Internal;
44

55
internal class SentryMauiEventProcessor : ISentryEventProcessor
66
{
7+
private readonly SentryMauiOptions _options;
8+
9+
public SentryMauiEventProcessor(SentryMauiOptions options)
10+
{
11+
_options = options;
12+
}
13+
714
public SentryEvent Process(SentryEvent @event)
815
{
9-
// Set SDK name and version for MAUI
1016
@event.Sdk.Name = Constants.SdkName;
1117
@event.Sdk.Version = Constants.SdkVersion;
18+
@event.Contexts.Device.ApplyMauiDeviceData(_options.DiagnosticLogger);
1219

1320
return @event;
1421
}

src/Sentry.Maui/Internal/SentryMauiOptionsSetup.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,6 @@ public override void Configure(SentryMauiOptions options)
2020
options.IsGlobalModeEnabled = true;
2121

2222
// We'll use an event processor to set things like SDK name
23-
options.AddEventProcessor(new SentryMauiEventProcessor());
23+
options.AddEventProcessor(new SentryMauiEventProcessor(options));
2424
}
2525
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
using Sentry.Android.Extensions;
2+
using Sentry.Extensibility;
3+
4+
namespace Sentry.Android;
5+
6+
internal class AndroidEventProcessor : ISentryEventProcessor, IDisposable
7+
{
8+
private readonly Java.IEventProcessor? _androidProcessor;
9+
private readonly Java.Hint _hint = new();
10+
11+
public AndroidEventProcessor(SentryAndroidOptions androidOptions)
12+
{
13+
// Locate the Android SDK's default event processor by its class
14+
// NOTE: This approach avoids hardcoding the class name (which could be obfuscated by proguard)
15+
_androidProcessor = androidOptions.EventProcessors.OfType<JavaObject>()
16+
.Where(o => o.Class == JavaClass.FromType(typeof(DefaultAndroidEventProcessor)))
17+
.Cast<Java.IEventProcessor>()
18+
.FirstOrDefault();
19+
20+
// TODO: This would be cleaner, but doesn't compile. Figure out why.
21+
// _androidProcessor = androidOptions.EventProcessors
22+
// .OfType<DefaultAndroidEventProcessor>()
23+
// .FirstOrDefault();
24+
}
25+
26+
public SentryEvent Process(SentryEvent @event)
27+
{
28+
// Get what information we can ourselves first
29+
@event.Contexts.Device.ApplyFromAndroidRuntime();
30+
31+
// Copy more information from the Android SDK
32+
if (_androidProcessor is { } androidProcessor)
33+
{
34+
// TODO: Can we gather more device data directly and remove this?
35+
36+
// Run a fake event through the Android processor, so we can get context info from the Android SDK.
37+
// We'll want to do this every time, so that all information is current. (ex: device orientation)
38+
using var e = new Java.SentryEvent();
39+
androidProcessor.Process(e, _hint);
40+
41+
// Copy what we need to the managed event
42+
if (e.Contexts.Device is { } device)
43+
{
44+
@event.Contexts.Device.ApplyFromSentryAndroidSdk(device);
45+
}
46+
}
47+
48+
return @event;
49+
}
50+
51+
public void Dispose()
52+
{
53+
_androidProcessor?.Dispose();
54+
_hint.Dispose();
55+
}
56+
}
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
using Sentry.Protocol;
2+
3+
namespace Sentry.Android.Extensions;
4+
5+
internal static class DeviceExtensions
6+
{
7+
public static void ApplyFromAndroidRuntime(this Device device)
8+
{
9+
device.Manufacturer ??= AndroidBuild.Manufacturer;
10+
device.Brand ??= AndroidBuild.Brand;
11+
device.Model ??= AndroidBuild.Model;
12+
13+
if (AndroidBuild.SupportedAbis is { } abis)
14+
{
15+
device.Architecture ??= abis[0];
16+
}
17+
else
18+
{
19+
#pragma warning disable CS0618 // Type or member is obsolete
20+
device.Architecture ??= AndroidBuild.CpuAbi;
21+
#pragma warning restore CS0618 // Type or member is obsolete
22+
}
23+
}
24+
25+
public static void ApplyFromSentryAndroidSdk(this Device device, Java.Protocol.Device d)
26+
{
27+
// We already have these above
28+
// device.Manufacturer ??= d.Manufacturer;
29+
// device.Brand ??= d.Brand;
30+
// device.Model ??= d.Model;
31+
// device.Architecture ??= d.GetArchs()?.FirstOrDefault();
32+
33+
device.Name ??= d.Name;
34+
device.Family ??= d.Family;
35+
device.ModelId ??= d.ModelId;
36+
device.BatteryLevel ??= d.BatteryLevel?.ShortValue();
37+
device.IsCharging ??= d.IsCharging()?.BooleanValue();
38+
device.IsOnline ??= d.IsOnline()?.BooleanValue();
39+
device.Orientation ??= d.Orientation?.ToDeviceOrientation();
40+
device.Simulator ??= d.IsSimulator()?.BooleanValue();
41+
device.MemorySize ??= d.MemorySize?.LongValue();
42+
device.FreeMemory ??= d.FreeMemory?.LongValue();
43+
device.UsableMemory ??= d.UsableMemory?.LongValue();
44+
device.LowMemory ??= d.IsLowMemory()?.BooleanValue();
45+
device.StorageSize ??= d.StorageSize?.LongValue();
46+
device.FreeStorage ??= d.FreeStorage?.LongValue();
47+
device.ExternalStorageSize ??= d.ExternalStorageSize?.LongValue();
48+
device.ExternalFreeStorage ??= d.ExternalFreeStorage?.LongValue();
49+
device.ScreenResolution ??= $"{d.ScreenWidthPixels}x{d.ScreenHeightPixels}";
50+
device.ScreenDensity ??= d.ScreenDensity?.FloatValue();
51+
device.ScreenDpi ??= d.ScreenDpi?.IntValue();
52+
device.BootTime ??= d.BootTime?.ToDateTimeOffset();
53+
device.DeviceUniqueIdentifier ??= d.Id;
54+
55+
// TODO: Can we get these from somewhere?
56+
//device.ProcessorCount ??= ?
57+
//device.CpuDescription ??= ?
58+
//device.ProcessorFrequency ??= ?
59+
//device.DeviceType ??= ?
60+
//device.BatteryStatus ??= ?
61+
//device.SupportsVibration ??= ?
62+
//device.SupportsAccelerometer ??= ?
63+
//device.SupportsGyroscope ??= ?
64+
//device.SupportsAudio ??= ?
65+
//device.SupportsLocationService ??= ?
66+
67+
}
68+
69+
public static DeviceOrientation ToDeviceOrientation(this Java.Protocol.Device.DeviceOrientation orientation) =>
70+
orientation.Name() switch
71+
{
72+
"PORTRAIT" => DeviceOrientation.Portrait,
73+
"LANDSCAPE" => DeviceOrientation.Landscape,
74+
_ => throw new ArgumentOutOfRangeException(nameof(orientation), orientation.Name(), message: default)
75+
};
76+
}

src/Sentry/Android/SentrySdk.cs

Lines changed: 14 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -29,30 +29,14 @@ public static IDisposable Init(AndroidContext context, Action<SentryOptions>? co
2929
/// <returns>An object that should be disposed when the application terminates.</returns>
3030
public static IDisposable Init(AndroidContext context, SentryOptions options)
3131
{
32-
// TODO: Pause/Resume
33-
options.AutoSessionTracking = true;
34-
options.IsGlobalModeEnabled = true;
35-
options.AddEventProcessor(new DelegateEventProcessor(evt =>
36-
{
37-
if (AndroidBuild.SupportedAbis is { } abis)
38-
{
39-
evt.Contexts.Device.Architecture = abis[0];
40-
}
41-
else
42-
{
43-
#pragma warning disable CS0618 // Type or member is obsolete
44-
evt.Contexts.Device.Architecture = AndroidBuild.CpuAbi;
45-
#pragma warning restore CS0618 // Type or member is obsolete
46-
}
47-
48-
evt.Contexts.Device.Manufacturer = AndroidBuild.Manufacturer;
49-
50-
return evt;
51-
}));
52-
32+
// Init the Java Android SDK first
33+
SentryAndroidOptions? androidOptions = null;
5334
SentryAndroid.Init(context, new JavaLogger(options),
5435
new OptionsConfigurationCallback(o =>
5536
{
37+
// Capture the android options reference on the outer scope
38+
androidOptions = o;
39+
5640
// TODO: Should we set the DistinctId to match the one used by GlobalSessionManager?
5741
//o.DistinctId = ?
5842

@@ -168,10 +152,17 @@ public static IDisposable Init(AndroidContext context, SentryOptions options)
168152
o.AddIgnoredExceptionForType(JavaClass.ForName("android.runtime.JavaProxyThrowable"));
169153
}));
170154

171-
options.CrashedLastRun = () => Java.Sentry.IsCrashedLastRun()?.BooleanValue() is true;
172-
155+
// Make sure we capture managed exceptions from the Android environment
173156
AndroidEnvironment.UnhandledExceptionRaiser += AndroidEnvironment_UnhandledExceptionRaiser;
174157

158+
// Set options for the managed SDK
159+
options.AutoSessionTracking = true;
160+
options.IsGlobalModeEnabled = true;
161+
options.AddEventProcessor(new AndroidEventProcessor(androidOptions!));
162+
options.CrashedLastRun = () => Java.Sentry.IsCrashedLastRun()?.BooleanValue() is true;
163+
// TODO: Pause/Resume
164+
165+
// Init the managed SDK
175166
return Init(options);
176167
}
177168

src/Sentry/Android/Transforms/Metadata.xml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,9 @@
4646
<attr path="/api/package[@name='io.sentry']/class[@name='SentryTraceHeader']/field[@name='SENTRY_TRACE_HEADER']" name="managedName">SentryTraceHeaderName</attr>
4747
<attr path="/api/package[@name='io.sentry']/class[@name='TraceStateHeader']/field[@name='TRACE_STATE_HEADER']" name="managedName">TraceStateHeaderName</attr>
4848

49+
<!-- Fix visibility of this type, for use in AndroidEventProcessor.cs -->
50+
<attr path="/api/package[@name='io.sentry.android.core']/class[@name='DefaultAndroidEventProcessor']" name="visibility">internal</attr>
51+
4952
<!--
5053
The remaining APIS are removed to prevent various errors/warnings.
5154
TODO: Find other workarounds for each one, rather than removing the APIs.

src/Sentry/Protocol/Device.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -209,10 +209,12 @@ public sealed class Device : IJsonSerializable
209209
/// </summary>
210210
/// <example>
211211
/// iOS: UIDevice.identifierForVendor (UUID)
212-
/// Android: md5 of ANDROID_ID
212+
/// Android: The generated Installation ID
213213
/// Windows Store Apps: AdvertisingManager::AdvertisingId (possible fallback to HardwareIdentification::GetPackageSpecificToken().Id)
214214
/// Windows Standalone: hash from the concatenation of strings taken from Computer System Hardware Classes
215215
/// </example>
216+
/// TODO: Investigate - Do ALL platforms now return a generated installation ID?
217+
/// See https://github.com/getsentry/sentry-java/pull/1455
216218
public string? DeviceUniqueIdentifier { get; set; }
217219

218220
/// <summary>

0 commit comments

Comments
 (0)