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

fix(MAUI): Automatically captured breadcrumbs #2900

Merged
merged 25 commits into from
Dec 1, 2023
Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,13 @@

## Unreleased

### Fixes

- Reworked automatic breadcrumb creation for MAUI. ([#2900](https://github.com/getsentry/sentry-dotnet/pull/2900))
- The SDK no longer uses on reflection to bind to all public element events. This also fixes issues where the SDK would consume third-party events.
- Added `CreateElementEventsBreadcrumbs` to the SentryMauiOptions to allow users to opt-in automatic breadcrumb creation for `BindingContextChanged`, `ChildAdded`, `ChildRemoved` and `ParentChanged` on `Element`.
- Reduced amount of automatic breadcrumbs by limiting the amount of bindings created in `VisualElement`, `Window`, `Shell`, `Page` and `Button`.

### Features

- Native crash reporting on NativeAOT published apps (Windows, Linux, macOS). ([#2887](https://github.com/getsentry/sentry-dotnet/pull/2887))
Expand Down
1 change: 0 additions & 1 deletion samples/Sentry.Samples.Maui/MainPage.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -88,4 +88,3 @@ private void OnNativeCrashClicked(object sender, EventArgs e)
#endif
}
}

2 changes: 2 additions & 0 deletions src/Sentry.Maui/BindableSentryMauiOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,14 @@ internal class BindableSentryMauiOptions : BindableSentryLoggingOptions
public bool? IncludeTextInBreadcrumbs { get; set; }
public bool? IncludeTitleInBreadcrumbs { get; set; }
public bool? IncludeBackgroundingStateInBreadcrumbs { get; set; }
public bool? CreateElementEventsBreadcrumbs { get; set; } = false;

public void ApplyTo(SentryMauiOptions options)
{
base.ApplyTo(options);
options.IncludeTextInBreadcrumbs = IncludeTextInBreadcrumbs ?? options.IncludeTextInBreadcrumbs;
options.IncludeTitleInBreadcrumbs = IncludeTitleInBreadcrumbs ?? options.IncludeTitleInBreadcrumbs;
options.IncludeBackgroundingStateInBreadcrumbs = IncludeBackgroundingStateInBreadcrumbs?? options.IncludeBackgroundingStateInBreadcrumbs;
options.CreateElementEventsBreadcrumbs = CreateElementEventsBreadcrumbs?? options.CreateElementEventsBreadcrumbs;
bitsandfoxes marked this conversation as resolved.
Show resolved Hide resolved
}
}
20 changes: 0 additions & 20 deletions src/Sentry.Maui/Internal/IMauiEventsBinder.cs

This file was deleted.

663 changes: 366 additions & 297 deletions src/Sentry.Maui/Internal/MauiEventsBinder.cs

Large diffs are not rendered by default.

53 changes: 36 additions & 17 deletions src/Sentry.Maui/SentryMauiAppBuilderExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ public static MauiAppBuilder UseSentry(this MauiAppBuilder builder, string dsn)
services.AddSingleton<IMauiInitializeService, SentryMauiInitializer>();
services.AddSingleton<IConfigureOptions<SentryMauiOptions>, SentryMauiOptionsSetup>();
services.AddSingleton<Disposer>();
services.TryAddSingleton<IMauiEventsBinder, MauiEventsBinder>();
services.TryAddSingleton<MauiEventsBinder>();

services.AddSentry<SentryMauiOptions>();

Expand All @@ -78,27 +78,46 @@ private static void RegisterMauiEventsBinder(this MauiAppBuilder builder)
builder.ConfigureLifecycleEvents(events =>
{
#if __IOS__
events.AddiOS(lifecycle => lifecycle.WillFinishLaunching((application, launchOptions) =>
events.AddiOS(lifecycle =>
{
// A bit of hackery here, because we can't mock UIKit.UIApplication in tests.
var platformApplication = application != null!
? application.Delegate as IPlatformApplication
: launchOptions["application"] as IPlatformApplication;

platformApplication?.BindMauiEvents();
return true;
}));
lifecycle.WillFinishLaunching((application, launchOptions) =>
{
// A bit of hackery here, because we can't mock UIKit.UIApplication in tests.
var platformApplication = application != null!
? application.Delegate as IPlatformApplication
: launchOptions["application"] as IPlatformApplication;

platformApplication?.HandleMauiEvents();
return true;
});
lifecycle.WillTerminate(application =>
{
if (application == null!)
{
return;
}

var platformApplication = application.Delegate as IPlatformApplication;
platformApplication?.HandleMauiEvents(bind: false);
});
});
#elif ANDROID
events.AddAndroid(lifecycle => lifecycle.OnApplicationCreating(application =>
(application as IPlatformApplication)?.BindMauiEvents()));
events.AddAndroid(lifecycle =>
{
lifecycle.OnApplicationCreating(application => (application as IPlatformApplication)?.HandleMauiEvents());
lifecycle.OnDestroy(application => (application as IPlatformApplication)?.HandleMauiEvents(bind: false));
});
#elif WINDOWS
events.AddWindows(lifecycle => lifecycle.OnLaunching((application, _) =>
(application as IPlatformApplication)?.BindMauiEvents()));
events.AddWindows(lifecycle =>
{
lifecycle.OnLaunching((application, _) => (application as IPlatformApplication)?.HandleMauiEvents());
lifecycle.OnClosed((application, _) => (application as IPlatformApplication)?.HandleMauiEvents(bind: false));
});
#endif
});
}

private static void BindMauiEvents(this IPlatformApplication platformApplication)
private static void HandleMauiEvents(this IPlatformApplication platformApplication, bool bind = true)
{
// We need to resolve the application manually, because it's not necessarily
// set on platformApplication.Application at this point in the lifecycle.
Expand All @@ -115,7 +134,7 @@ private static void BindMauiEvents(this IPlatformApplication platformApplication
}

// Bind the events
var binder = services.GetRequiredService<IMauiEventsBinder>();
binder.BindApplicationEvents(application);
var binder = services.GetRequiredService<MauiEventsBinder>();
binder.HandleApplicationEvents(application, bind);
}
}
7 changes: 7 additions & 0 deletions src/Sentry.Maui/SentryMauiOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -49,4 +49,11 @@ public SentryMauiOptions()
/// The default is <c>false</c> (exclude).
/// </summary>
public bool IncludeBackgroundingStateInBreadcrumbs { get; set; }

/// <summary>
/// Gets or sets whether the SDK automatically binds to common <see cref=" Microsoft.Maui.Controls.Element"/> events
/// like 'ChildAdded', 'ChildRemoved', 'ParentChanged' and 'BindingContextChanged'.
/// Use caution when enabling, as depending on your application this might incur a performance overhead.
/// </summary>
public bool CreateElementEventsBreadcrumbs { get; set; } = false;
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ namespace Sentry.Maui
public class SentryMauiOptions : Sentry.Extensions.Logging.SentryLoggingOptions
{
public SentryMauiOptions() { }
public bool CreateElementEventsBreadcrumbs { get; set; }
public bool IncludeBackgroundingStateInBreadcrumbs { get; set; }
public bool IncludeTextInBreadcrumbs { get; set; }
public bool IncludeTitleInBreadcrumbs { get; set; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ namespace Sentry.Maui
public class SentryMauiOptions : Sentry.Extensions.Logging.SentryLoggingOptions
{
public SentryMauiOptions() { }
public bool CreateElementEventsBreadcrumbs { get; set; }
public bool IncludeBackgroundingStateInBreadcrumbs { get; set; }
public bool IncludeTextInBreadcrumbs { get; set; }
public bool IncludeTitleInBreadcrumbs { get; set; }
Expand Down
101 changes: 94 additions & 7 deletions test/Sentry.Maui.Tests/MauiEventsBinderTests.Application.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
using Microsoft.Extensions.Options;
using Sentry.Maui.Internal;
using Sentry.Maui.Tests.Mocks;

Expand All @@ -12,8 +11,9 @@ public partial class MauiEventsBinderTests
public void Application_ChildElementEvents_AddsBreadcrumb(string eventName)
{
// Arrange
_fixture.Options.CreateElementEventsBreadcrumbs = true;
var application = MockApplication.Create();
_fixture.Binder.BindApplicationEvents(application);
_fixture.Binder.HandleApplicationEvents(application);

var element = Substitute.For<Element>();

Expand All @@ -29,14 +29,38 @@ public void Application_ChildElementEvents_AddsBreadcrumb(string eventName)
crumb.Data.Should().Contain("Element", element.ToString());
}

[Theory]
[InlineData(nameof(Application.ChildAdded))]
[InlineData(nameof(Application.ChildRemoved))]
public void Application_UnbindChildElementEvents_DoesNotAddBreadcrumb(string eventName)
{
// Arrange
_fixture.Options.CreateElementEventsBreadcrumbs = true;
var application = MockApplication.Create();
_fixture.Binder.HandleApplicationEvents(application);

var element = Substitute.For<Element>();
application.RaiseEvent(eventName, new ElementEventArgs(element));
Assert.Equal(1, _fixture.Scope.Breadcrumbs.Count); // Sanity check

_fixture.Binder.HandleApplicationEvents(application, bind: false);

// Act
application.RaiseEvent(eventName, new ElementEventArgs(element));

// Assert
application.RaiseEvent(eventName, new ElementEventArgs(element));
Assert.Equal(1, _fixture.Scope.Breadcrumbs.Count);
}

[Theory]
[InlineData(nameof(Application.PageAppearing))]
[InlineData(nameof(Application.PageDisappearing))]
public void Application_PageEvents_AddsBreadcrumb(string eventName)
{
// Arrange
var application = MockApplication.Create();
_fixture.Binder.BindApplicationEvents(application);
_fixture.Binder.HandleApplicationEvents(application);
var page = new ContentPage
{
StyleId = "TestPage"
Expand All @@ -55,13 +79,38 @@ public void Application_PageEvents_AddsBreadcrumb(string eventName)
crumb.Data.Should().Contain("Page.Name", page.StyleId);
}

[Theory]
[InlineData(nameof(Application.PageAppearing))]
[InlineData(nameof(Application.PageDisappearing))]
public void Application_UnbindPageEvents_DoesNotAddBreadcrumb(string eventName)
{
// Arrange
var application = MockApplication.Create();
_fixture.Binder.HandleApplicationEvents(application);
var page = new ContentPage
{
StyleId = "TestPage"
};

application.RaiseEvent(eventName, page);
Assert.Equal(1, _fixture.Scope.Breadcrumbs.Count); // Sanity check

_fixture.Binder.HandleApplicationEvents(application, bind: false);

// Act
application.RaiseEvent(eventName, page);

// Assert
Assert.Equal(1, _fixture.Scope.Breadcrumbs.Count);
}

[Theory]
[MemberData(nameof(ApplicationModalEventsData))]
public void Application_ModalEvents_AddsBreadcrumb(string eventName, object eventArgs)
{
// Arrange
var application = MockApplication.Create();
_fixture.Binder.BindApplicationEvents(application);
_fixture.Binder.HandleApplicationEvents(application);

// Act
application.RaiseEvent(eventName, eventArgs);
Expand All @@ -76,6 +125,26 @@ public void Application_ModalEvents_AddsBreadcrumb(string eventName, object even
crumb.Data.Should().Contain("Modal.Name", "TestModalPage");
}

[Theory]
[MemberData(nameof(ApplicationModalEventsData))]
public void Application_UnbindModalEvents_DoesNotAddBreadcrumb(string eventName, object eventArgs)
{
// Arrange
var application = MockApplication.Create();
_fixture.Binder.HandleApplicationEvents(application);

application.RaiseEvent(eventName, eventArgs);
Assert.Equal(1, _fixture.Scope.Breadcrumbs.Count); // Sanity check

_fixture.Binder.HandleApplicationEvents(application, bind: false);

// Act
application.RaiseEvent(eventName, eventArgs);

// Assert
Assert.Equal(1, _fixture.Scope.Breadcrumbs.Count);
}

public static IEnumerable<object[]> ApplicationModalEventsData
{
get
Expand All @@ -88,9 +157,7 @@ public static IEnumerable<object[]> ApplicationModalEventsData
return new List<object[]>
{
// Note, these are distinct from the Window events with the same names.
new object[] {nameof(Application.ModalPushing), new ModalPushingEventArgs(modelPage)},
new object[] {nameof(Application.ModalPushed), new ModalPushedEventArgs(modelPage)},
new object[] {nameof(Application.ModalPopping), new ModalPoppingEventArgs(modelPage)},
new object[] {nameof(Application.ModalPopped), new ModalPoppedEventArgs(modelPage)}
};
}
Expand All @@ -102,7 +169,7 @@ public void Application_RequestedThemeChanged_AddsBreadcrumb()
// Arrange
var application = MockApplication.Create();
application.UserAppTheme = AppTheme.Unspecified;
_fixture.Binder.BindApplicationEvents(application);
_fixture.Binder.HandleApplicationEvents(application);

// Act
application.UserAppTheme = AppTheme.Dark;
Expand All @@ -115,4 +182,24 @@ public void Application_RequestedThemeChanged_AddsBreadcrumb()
Assert.Equal(MauiEventsBinder.RenderingCategory, crumb.Category);
crumb.Data.Should().Contain("RequestedTheme", AppTheme.Dark.ToString());
}

[Fact]
public void Application_UnbindRequestedThemeChanged_DoesNotAddBreadcrumb()
{
// Arrange
var application = MockApplication.Create();
application.UserAppTheme = AppTheme.Unspecified;
_fixture.Binder.HandleApplicationEvents(application);

application.UserAppTheme = AppTheme.Dark;
Assert.Equal(1, _fixture.Scope.Breadcrumbs.Count); // Sanity check

_fixture.Binder.HandleApplicationEvents(application, bind: false);

// Act
application.UserAppTheme = AppTheme.Light;

// Assert
Assert.Equal(1, _fixture.Scope.Breadcrumbs.Count);
}
}
26 changes: 25 additions & 1 deletion test/Sentry.Maui.Tests/MauiEventsBinderTests.Button.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ public void Button_CommonEvents_AddsBreadcrumb(string eventName)
{
StyleId = "button"
};
_fixture.Binder.BindButtonEvents(button);
_fixture.Binder.HandleButtonEvents(button);

// Act
button.RaiseEvent(eventName, EventArgs.Empty);
Expand All @@ -28,4 +28,28 @@ public void Button_CommonEvents_AddsBreadcrumb(string eventName)
Assert.Equal(MauiEventsBinder.UserActionCategory, crumb.Category);
crumb.Data.Should().Contain($"{nameof(Button)}.Name", "button");
}

[Theory]
[InlineData(nameof(Button.Clicked))]
[InlineData(nameof(Button.Pressed))]
[InlineData(nameof(Button.Released))]
public void Button_UnbindCommonEvents_DoesNotAddBreadcrumb(string eventName)
{
// Arrange
var button = new Button
{
StyleId = "button"
};
_fixture.Binder.HandleButtonEvents(button);
button.RaiseEvent(eventName, EventArgs.Empty);
Assert.Equal(1, _fixture.Scope.Breadcrumbs.Count); // Sanity check

_fixture.Binder.HandleButtonEvents(button, bind: false);

// Act
button.RaiseEvent(eventName, EventArgs.Empty);

// Assert
Assert.Equal(1, _fixture.Scope.Breadcrumbs.Count);
}
}