Skip to content
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 @@ -4,6 +4,7 @@

### Features

- Users can now register their own MAUI controls for breadcrumb creation ([#3997](https://github.com/getsentry/sentry-dotnet/pull/3997))
- Serilog scope properties are now sent with Sentry events ([#3976](https://github.com/getsentry/sentry-dotnet/pull/3976))
- The sample seed used for sampling decisions is now propagated, for use in downstream custom trace samplers ([#3951](https://github.com/getsentry/sentry-dotnet/pull/3951))

Expand Down
35 changes: 35 additions & 0 deletions src/Sentry.Maui/IMauiElementEventBinder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
namespace Sentry.Maui;

/// <summary>
/// Bind to MAUI controls to generate breadcrumbs and other metrics
/// </summary>
public interface IMauiElementEventBinder
{
/// <summary>
/// Bind to an element
/// </summary>
/// <param name="element"></param>
/// <param name="addBreadcrumb">
/// This adds a breadcrumb to the sentry hub
/// NOTE: we will override the type, timestamp, and category of the breadcrumb
/// </param>
void Bind(VisualElement element, Action<BreadcrumbEvent> addBreadcrumb);

/// <summary>
/// Unbind the element because MAUI is removing the page
/// </summary>
/// <param name="element"></param>
void UnBind(VisualElement element);
}

/// <summary>
/// Breadcrumb arguments
/// </summary>
/// <param name="Sender"></param>
/// <param name="EventName"></param>
/// <param name="ExtraData"></param>
public record BreadcrumbEvent(
object? Sender,
string EventName,
params IEnumerable<(string Key, string Value)>[] ExtraData
);
41 changes: 41 additions & 0 deletions src/Sentry.Maui/Internal/MauiButtonEventsBinder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
namespace Sentry.Maui.Internal;

/// <inheritdoc />
public class MauiButtonEventsBinder : IMauiElementEventBinder
{
private Action<BreadcrumbEvent>? addBreadcrumbCallback;

/// <inheritdoc />
public void Bind(VisualElement element, Action<BreadcrumbEvent> addBreadcrumb)
{
addBreadcrumbCallback = addBreadcrumb;

if (element is Button button)
{
button.Clicked += OnButtonOnClicked;
button.Pressed += OnButtonOnPressed;
button.Released += OnButtonOnReleased;
}
}

/// <inheritdoc />
public void UnBind(VisualElement element)
{
if (element is Button button)
{
button.Clicked -= OnButtonOnClicked;
button.Pressed -= OnButtonOnPressed;
button.Released -= OnButtonOnReleased;
}
}


private void OnButtonOnClicked(object? sender, EventArgs _)
=> addBreadcrumbCallback?.Invoke(new(sender, nameof(Button.Clicked)));

private void OnButtonOnPressed(object? sender, EventArgs _)
=> addBreadcrumbCallback?.Invoke(new(sender, nameof(Button.Pressed)));

private void OnButtonOnReleased(object? sender, EventArgs _)
=> addBreadcrumbCallback?.Invoke(new(sender, nameof(Button.Released)));
}
68 changes: 32 additions & 36 deletions src/Sentry.Maui/Internal/MauiEventsBinder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ internal class MauiEventsBinder : IMauiEventsBinder
{
private readonly IHub _hub;
private readonly SentryMauiOptions _options;
private readonly IEnumerable<IMauiElementEventBinder> _elementEventBinders;

// https://develop.sentry.dev/sdk/event-payloads/breadcrumbs/#breadcrumb-types
// https://github.com/getsentry/sentry/blob/master/static/app/types/breadcrumbs.tsx
Expand All @@ -22,10 +23,11 @@ internal class MauiEventsBinder : IMauiEventsBinder
internal const string RenderingCategory = "ui.rendering";
internal const string UserActionCategory = "ui.useraction";

public MauiEventsBinder(IHub hub, IOptions<SentryMauiOptions> options)
public MauiEventsBinder(IHub hub, IOptions<SentryMauiOptions> options, IEnumerable<IMauiElementEventBinder> elementEventBinders)
{
_hub = hub;
_options = options.Value;
_elementEventBinders = elementEventBinders;
}

public void HandleApplicationEvents(Application application, bool bind = true)
Expand Down Expand Up @@ -70,7 +72,7 @@ public void HandleApplicationEvents(Application application, bool bind = true)
}
}

private void OnApplicationOnDescendantAdded(object? _, ElementEventArgs e)
internal void OnApplicationOnDescendantAdded(object? _, ElementEventArgs e)
{
if (_options.CreateElementEventsBreadcrumbs)
{
Expand All @@ -96,15 +98,30 @@ private void OnApplicationOnDescendantAdded(object? _, ElementEventArgs e)
case Page page:
HandlePageEvents(page);
break;
case Button button:
HandleButtonEvents(button);
default:
Comment thread
aritchie marked this conversation as resolved.
if (e.Element is VisualElement ve)
{
foreach (var binder in _elementEventBinders)
{
binder.Bind(ve, OnBreadcrumbCreateCallback);
}
}
break;

// TODO: Attach to specific events on more control types
}
}

private void OnApplicationOnDescendantRemoved(object? _, ElementEventArgs e)
internal void OnBreadcrumbCreateCallback(BreadcrumbEvent breadcrumb)
{
_hub.AddBreadcrumbForEvent(
_options,
breadcrumb.Sender,
breadcrumb.EventName,
Copy link
Copy Markdown
Contributor

@albyrock87 albyrock87 Jun 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jamescrosswell I'm sorry for this late review but I just realized there's a huge bug here.

What about breadcrumb.ExtraData? I think you completely missed it.

Also I don't find the ExtraData type very convenient.

I think that this could be more convenient:

public record BreadcrumbEvent(
    object? Sender,
    string EventName,
    params (string Key, string Value)[] ExtraData
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

internal void OnBreadcrumbCreateCallback(BreadcrumbEvent breadcrumb)
    {
        void AddExtraData(Dictionary<string, string> extraData)
        {
            foreach (var data in breadcrumb.ExtraData.SelectMany(d => d))
            {
                extraData[data.Key] = data.Value;
            }
        }

        _hub.AddBreadcrumbForEvent(
            _options,
            breadcrumb.Sender,
            breadcrumb.EventName,
            UserType,
            UserActionCategory,
            AddExtraData
        );
    }

Copy link
Copy Markdown
Collaborator

@jamescrosswell jamescrosswell Jun 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

UserType,
UserActionCategory
);
}

internal void OnApplicationOnDescendantRemoved(object? _, ElementEventArgs e)
{
// All elements have a set of common events we can hook
HandleElementEvents(e.Element, bind: false);
Expand All @@ -127,8 +144,14 @@ private void OnApplicationOnDescendantRemoved(object? _, ElementEventArgs e)
case Page page:
HandlePageEvents(page, bind: false);
break;
case Button button:
HandleButtonEvents(button, bind: false);
default:
if (e.Element is VisualElement ve)
{
foreach (var binder in _elementEventBinders)
{
binder.UnBind(ve);
}
}
break;
}
}
Expand Down Expand Up @@ -279,22 +302,6 @@ internal void HandlePageEvents(Page page, bool bind = true)
}
}

internal void HandleButtonEvents(Button button, bool bind = true)
{
if (bind)
{
button.Clicked += OnButtonOnClicked;
button.Pressed += OnButtonOnPressed;
button.Released += OnButtonOnReleased;
}
else
{
button.Clicked -= OnButtonOnClicked;
button.Pressed -= OnButtonOnPressed;
button.Released -= OnButtonOnReleased;
}
}

// Application Events

private void OnApplicationOnPageAppearing(object? sender, Page page) =>
Expand Down Expand Up @@ -424,15 +431,4 @@ private void OnPageOnNavigatedTo(object? sender, NavigatedToEventArgs e) =>

private void OnPageOnLayoutChanged(object? sender, EventArgs _) =>
_hub.AddBreadcrumbForEvent(_options, sender, nameof(Page.LayoutChanged), SystemType, RenderingCategory);

// Button Events

private void OnButtonOnClicked(object? sender, EventArgs _) =>
_hub.AddBreadcrumbForEvent(_options, sender, nameof(Button.Clicked), UserType, UserActionCategory);

private void OnButtonOnPressed(object? sender, EventArgs _) =>
_hub.AddBreadcrumbForEvent(_options, sender, nameof(Button.Pressed), UserType, UserActionCategory);

private void OnButtonOnReleased(object? sender, EventArgs _) =>
_hub.AddBreadcrumbForEvent(_options, sender, nameof(Button.Released), UserType, UserActionCategory);
}
41 changes: 41 additions & 0 deletions src/Sentry.Maui/Internal/MauiImageButtonEventsBinder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
namespace Sentry.Maui.Internal;

/// <inheritdoc />
public class MauiImageButtonEventsBinder : IMauiElementEventBinder
{
private Action<BreadcrumbEvent>? addBreadcrumbCallback;

/// <inheritdoc />
public void Bind(VisualElement element, Action<BreadcrumbEvent> addBreadcrumb)
{
addBreadcrumbCallback = addBreadcrumb;

if (element is ImageButton image)
{
image.Clicked += OnButtonOnClicked;
image.Pressed += OnButtonOnPressed;
image.Released += OnButtonOnReleased;
}
}

/// <inheritdoc />
public void UnBind(VisualElement element)
{
if (element is ImageButton image)
{
image.Clicked -= OnButtonOnClicked;
image.Pressed -= OnButtonOnPressed;
image.Released -= OnButtonOnReleased;
}
}


private void OnButtonOnClicked(object? sender, EventArgs _)
=> addBreadcrumbCallback?.Invoke(new(sender, nameof(ImageButton.Clicked)));

private void OnButtonOnPressed(object? sender, EventArgs _)
=> addBreadcrumbCallback?.Invoke(new(sender, nameof(ImageButton.Pressed)));

private void OnButtonOnReleased(object? sender, EventArgs _)
=> addBreadcrumbCallback?.Invoke(new(sender, nameof(ImageButton.Released)));
}
3 changes: 3 additions & 0 deletions src/Sentry.Maui/SentryMauiAppBuilderExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,9 @@ public static MauiAppBuilder UseSentry(this MauiAppBuilder builder,
services.AddSingleton<IMauiInitializeService, SentryMauiInitializer>();
services.AddSingleton<IConfigureOptions<SentryMauiOptions>, SentryMauiOptionsSetup>();
services.AddSingleton<Disposer>();

services.AddSingleton<IMauiElementEventBinder, MauiButtonEventsBinder>();
services.AddSingleton<IMauiElementEventBinder, MauiImageButtonEventsBinder>();
services.TryAddSingleton<IMauiEventsBinder, MauiEventsBinder>();

services.AddSentry<SentryMauiOptions>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,23 @@
}
namespace Sentry.Maui
{
public class BreadcrumbEvent : System.IEquatable<Sentry.Maui.BreadcrumbEvent>
{
public BreadcrumbEvent(object? Sender, string EventName, [System.Runtime.CompilerServices.TupleElementNames(new string[] {
"Key",
"Value"})] params System.Collections.Generic.IEnumerable<System.ValueTuple<string, string>>[] ExtraData) { }
public string EventName { get; init; }
[System.Runtime.CompilerServices.TupleElementNames(new string[] {
"Key",
"Value"})]
public System.Collections.Generic.IEnumerable<System.ValueTuple<string, string>>[] ExtraData { get; init; }
public object? Sender { get; init; }
}
public interface IMauiElementEventBinder
{
void Bind(Microsoft.Maui.Controls.VisualElement element, System.Action<Sentry.Maui.BreadcrumbEvent> addBreadcrumb);
void UnBind(Microsoft.Maui.Controls.VisualElement element);
}
public class SentryMauiOptions : Sentry.Extensions.Logging.SentryLoggingOptions
{
public SentryMauiOptions() { }
Expand All @@ -19,4 +36,19 @@ namespace Sentry.Maui
public bool IncludeTitleInBreadcrumbs { get; set; }
public void SetBeforeScreenshotCapture(System.Func<Sentry.SentryEvent, Sentry.SentryHint, bool> beforeCapture) { }
}
}
namespace Sentry.Maui.Internal
{
public class MauiButtonEventsBinder : Sentry.Maui.IMauiElementEventBinder
{
public MauiButtonEventsBinder() { }
public void Bind(Microsoft.Maui.Controls.VisualElement element, System.Action<Sentry.Maui.BreadcrumbEvent> addBreadcrumb) { }
public void UnBind(Microsoft.Maui.Controls.VisualElement element) { }
}
public class MauiImageButtonEventsBinder : Sentry.Maui.IMauiElementEventBinder
{
public MauiImageButtonEventsBinder() { }
public void Bind(Microsoft.Maui.Controls.VisualElement element, System.Action<Sentry.Maui.BreadcrumbEvent> addBreadcrumb) { }
public void UnBind(Microsoft.Maui.Controls.VisualElement element) { }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,23 @@
}
namespace Sentry.Maui
{
public class BreadcrumbEvent : System.IEquatable<Sentry.Maui.BreadcrumbEvent>
{
public BreadcrumbEvent(object? Sender, string EventName, [System.Runtime.CompilerServices.TupleElementNames(new string[] {
"Key",
"Value"})] params System.Collections.Generic.IEnumerable<System.ValueTuple<string, string>>[] ExtraData) { }
public string EventName { get; init; }
[System.Runtime.CompilerServices.TupleElementNames(new string[] {
"Key",
"Value"})]
public System.Collections.Generic.IEnumerable<System.ValueTuple<string, string>>[] ExtraData { get; init; }
public object? Sender { get; init; }
}
public interface IMauiElementEventBinder
{
void Bind(Microsoft.Maui.Controls.VisualElement element, System.Action<Sentry.Maui.BreadcrumbEvent> addBreadcrumb);
void UnBind(Microsoft.Maui.Controls.VisualElement element);
}
public class SentryMauiOptions : Sentry.Extensions.Logging.SentryLoggingOptions
{
public SentryMauiOptions() { }
Expand All @@ -19,4 +36,19 @@ namespace Sentry.Maui
public bool IncludeTitleInBreadcrumbs { get; set; }
public void SetBeforeScreenshotCapture(System.Func<Sentry.SentryEvent, Sentry.SentryHint, bool> beforeCapture) { }
}
}
namespace Sentry.Maui.Internal
{
public class MauiButtonEventsBinder : Sentry.Maui.IMauiElementEventBinder
{
public MauiButtonEventsBinder() { }
public void Bind(Microsoft.Maui.Controls.VisualElement element, System.Action<Sentry.Maui.BreadcrumbEvent> addBreadcrumb) { }
public void UnBind(Microsoft.Maui.Controls.VisualElement element) { }
}
public class MauiImageButtonEventsBinder : Sentry.Maui.IMauiElementEventBinder
{
public MauiImageButtonEventsBinder() { }
public void Bind(Microsoft.Maui.Controls.VisualElement element, System.Action<Sentry.Maui.BreadcrumbEvent> addBreadcrumb) { }
public void UnBind(Microsoft.Maui.Controls.VisualElement element) { }
}
}
9 changes: 6 additions & 3 deletions test/Sentry.Maui.Tests/MauiEventsBinderTests.Button.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ public void Button_CommonEvents_AddsBreadcrumb(string eventName)
{
StyleId = "button"
};
_fixture.Binder.HandleButtonEvents(button);
var el = new ElementEventArgs(button);
_fixture.Binder.OnApplicationOnDescendantAdded(null, el);

// Act
button.RaiseEvent(eventName, EventArgs.Empty);
Expand All @@ -40,11 +41,13 @@ public void Button_UnbindCommonEvents_DoesNotAddBreadcrumb(string eventName)
{
StyleId = "button"
};
_fixture.Binder.HandleButtonEvents(button);
var el = new ElementEventArgs(button);
_fixture.Binder.OnApplicationOnDescendantAdded(null, el);

button.RaiseEvent(eventName, EventArgs.Empty);
Assert.Single(_fixture.Scope.Breadcrumbs); // Sanity check

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

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