Skip to content

Commit fbdcce9

Browse files
authored
Add element binding extensibility (#3997)
* Add element binding extensibility * Update CHANGELOG.md * Add missed test updates * Fix tests * Add additional image button binder and tests * Update verified api approval tests
1 parent 0f24f0d commit fbdcce9

11 files changed

+289
-40
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
### Features
77

8+
- Users can now register their own MAUI controls for breadcrumb creation ([#3997](https://github.com/getsentry/sentry-dotnet/pull/3997))
89
- Serilog scope properties are now sent with Sentry events ([#3976](https://github.com/getsentry/sentry-dotnet/pull/3976))
910
- 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))
1011
- Add Azure Function UseSentry overloads for easier wire ups ([#3971](https://github.com/getsentry/sentry-dotnet/pull/3971))
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
namespace Sentry.Maui;
2+
3+
/// <summary>
4+
/// Bind to MAUI controls to generate breadcrumbs and other metrics
5+
/// </summary>
6+
public interface IMauiElementEventBinder
7+
{
8+
/// <summary>
9+
/// Bind to an element
10+
/// </summary>
11+
/// <param name="element"></param>
12+
/// <param name="addBreadcrumb">
13+
/// This adds a breadcrumb to the sentry hub
14+
/// NOTE: we will override the type, timestamp, and category of the breadcrumb
15+
/// </param>
16+
void Bind(VisualElement element, Action<BreadcrumbEvent> addBreadcrumb);
17+
18+
/// <summary>
19+
/// Unbind the element because MAUI is removing the page
20+
/// </summary>
21+
/// <param name="element"></param>
22+
void UnBind(VisualElement element);
23+
}
24+
25+
/// <summary>
26+
/// Breadcrumb arguments
27+
/// </summary>
28+
/// <param name="Sender"></param>
29+
/// <param name="EventName"></param>
30+
/// <param name="ExtraData"></param>
31+
public record BreadcrumbEvent(
32+
object? Sender,
33+
string EventName,
34+
params IEnumerable<(string Key, string Value)>[] ExtraData
35+
);
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
namespace Sentry.Maui.Internal;
2+
3+
/// <inheritdoc />
4+
public class MauiButtonEventsBinder : IMauiElementEventBinder
5+
{
6+
private Action<BreadcrumbEvent>? addBreadcrumbCallback;
7+
8+
/// <inheritdoc />
9+
public void Bind(VisualElement element, Action<BreadcrumbEvent> addBreadcrumb)
10+
{
11+
addBreadcrumbCallback = addBreadcrumb;
12+
13+
if (element is Button button)
14+
{
15+
button.Clicked += OnButtonOnClicked;
16+
button.Pressed += OnButtonOnPressed;
17+
button.Released += OnButtonOnReleased;
18+
}
19+
}
20+
21+
/// <inheritdoc />
22+
public void UnBind(VisualElement element)
23+
{
24+
if (element is Button button)
25+
{
26+
button.Clicked -= OnButtonOnClicked;
27+
button.Pressed -= OnButtonOnPressed;
28+
button.Released -= OnButtonOnReleased;
29+
}
30+
}
31+
32+
33+
private void OnButtonOnClicked(object? sender, EventArgs _)
34+
=> addBreadcrumbCallback?.Invoke(new(sender, nameof(Button.Clicked)));
35+
36+
private void OnButtonOnPressed(object? sender, EventArgs _)
37+
=> addBreadcrumbCallback?.Invoke(new(sender, nameof(Button.Pressed)));
38+
39+
private void OnButtonOnReleased(object? sender, EventArgs _)
40+
=> addBreadcrumbCallback?.Invoke(new(sender, nameof(Button.Released)));
41+
}

src/Sentry.Maui/Internal/MauiEventsBinder.cs

Lines changed: 32 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ internal class MauiEventsBinder : IMauiEventsBinder
1111
{
1212
private readonly IHub _hub;
1313
private readonly SentryMauiOptions _options;
14+
private readonly IEnumerable<IMauiElementEventBinder> _elementEventBinders;
1415

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

25-
public MauiEventsBinder(IHub hub, IOptions<SentryMauiOptions> options)
26+
public MauiEventsBinder(IHub hub, IOptions<SentryMauiOptions> options, IEnumerable<IMauiElementEventBinder> elementEventBinders)
2627
{
2728
_hub = hub;
2829
_options = options.Value;
30+
_elementEventBinders = elementEventBinders;
2931
}
3032

3133
public void HandleApplicationEvents(Application application, bool bind = true)
@@ -70,7 +72,7 @@ public void HandleApplicationEvents(Application application, bool bind = true)
7072
}
7173
}
7274

73-
private void OnApplicationOnDescendantAdded(object? _, ElementEventArgs e)
75+
internal void OnApplicationOnDescendantAdded(object? _, ElementEventArgs e)
7476
{
7577
if (_options.CreateElementEventsBreadcrumbs)
7678
{
@@ -96,15 +98,30 @@ private void OnApplicationOnDescendantAdded(object? _, ElementEventArgs e)
9698
case Page page:
9799
HandlePageEvents(page);
98100
break;
99-
case Button button:
100-
HandleButtonEvents(button);
101+
default:
102+
if (e.Element is VisualElement ve)
103+
{
104+
foreach (var binder in _elementEventBinders)
105+
{
106+
binder.Bind(ve, OnBreadcrumbCreateCallback);
107+
}
108+
}
101109
break;
102-
103-
// TODO: Attach to specific events on more control types
104110
}
105111
}
106112

107-
private void OnApplicationOnDescendantRemoved(object? _, ElementEventArgs e)
113+
internal void OnBreadcrumbCreateCallback(BreadcrumbEvent breadcrumb)
114+
{
115+
_hub.AddBreadcrumbForEvent(
116+
_options,
117+
breadcrumb.Sender,
118+
breadcrumb.EventName,
119+
UserType,
120+
UserActionCategory
121+
);
122+
}
123+
124+
internal void OnApplicationOnDescendantRemoved(object? _, ElementEventArgs e)
108125
{
109126
// All elements have a set of common events we can hook
110127
HandleElementEvents(e.Element, bind: false);
@@ -127,8 +144,14 @@ private void OnApplicationOnDescendantRemoved(object? _, ElementEventArgs e)
127144
case Page page:
128145
HandlePageEvents(page, bind: false);
129146
break;
130-
case Button button:
131-
HandleButtonEvents(button, bind: false);
147+
default:
148+
if (e.Element is VisualElement ve)
149+
{
150+
foreach (var binder in _elementEventBinders)
151+
{
152+
binder.UnBind(ve);
153+
}
154+
}
132155
break;
133156
}
134157
}
@@ -279,22 +302,6 @@ internal void HandlePageEvents(Page page, bool bind = true)
279302
}
280303
}
281304

282-
internal void HandleButtonEvents(Button button, bool bind = true)
283-
{
284-
if (bind)
285-
{
286-
button.Clicked += OnButtonOnClicked;
287-
button.Pressed += OnButtonOnPressed;
288-
button.Released += OnButtonOnReleased;
289-
}
290-
else
291-
{
292-
button.Clicked -= OnButtonOnClicked;
293-
button.Pressed -= OnButtonOnPressed;
294-
button.Released -= OnButtonOnReleased;
295-
}
296-
}
297-
298305
// Application Events
299306

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

425432
private void OnPageOnLayoutChanged(object? sender, EventArgs _) =>
426433
_hub.AddBreadcrumbForEvent(_options, sender, nameof(Page.LayoutChanged), SystemType, RenderingCategory);
427-
428-
// Button Events
429-
430-
private void OnButtonOnClicked(object? sender, EventArgs _) =>
431-
_hub.AddBreadcrumbForEvent(_options, sender, nameof(Button.Clicked), UserType, UserActionCategory);
432-
433-
private void OnButtonOnPressed(object? sender, EventArgs _) =>
434-
_hub.AddBreadcrumbForEvent(_options, sender, nameof(Button.Pressed), UserType, UserActionCategory);
435-
436-
private void OnButtonOnReleased(object? sender, EventArgs _) =>
437-
_hub.AddBreadcrumbForEvent(_options, sender, nameof(Button.Released), UserType, UserActionCategory);
438434
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
namespace Sentry.Maui.Internal;
2+
3+
/// <inheritdoc />
4+
public class MauiImageButtonEventsBinder : IMauiElementEventBinder
5+
{
6+
private Action<BreadcrumbEvent>? addBreadcrumbCallback;
7+
8+
/// <inheritdoc />
9+
public void Bind(VisualElement element, Action<BreadcrumbEvent> addBreadcrumb)
10+
{
11+
addBreadcrumbCallback = addBreadcrumb;
12+
13+
if (element is ImageButton image)
14+
{
15+
image.Clicked += OnButtonOnClicked;
16+
image.Pressed += OnButtonOnPressed;
17+
image.Released += OnButtonOnReleased;
18+
}
19+
}
20+
21+
/// <inheritdoc />
22+
public void UnBind(VisualElement element)
23+
{
24+
if (element is ImageButton image)
25+
{
26+
image.Clicked -= OnButtonOnClicked;
27+
image.Pressed -= OnButtonOnPressed;
28+
image.Released -= OnButtonOnReleased;
29+
}
30+
}
31+
32+
33+
private void OnButtonOnClicked(object? sender, EventArgs _)
34+
=> addBreadcrumbCallback?.Invoke(new(sender, nameof(ImageButton.Clicked)));
35+
36+
private void OnButtonOnPressed(object? sender, EventArgs _)
37+
=> addBreadcrumbCallback?.Invoke(new(sender, nameof(ImageButton.Pressed)));
38+
39+
private void OnButtonOnReleased(object? sender, EventArgs _)
40+
=> addBreadcrumbCallback?.Invoke(new(sender, nameof(ImageButton.Released)));
41+
}

src/Sentry.Maui/SentryMauiAppBuilderExtensions.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,9 @@ public static MauiAppBuilder UseSentry(this MauiAppBuilder builder,
5454
services.AddSingleton<IMauiInitializeService, SentryMauiInitializer>();
5555
services.AddSingleton<IConfigureOptions<SentryMauiOptions>, SentryMauiOptionsSetup>();
5656
services.AddSingleton<Disposer>();
57+
58+
services.AddSingleton<IMauiElementEventBinder, MauiButtonEventsBinder>();
59+
services.AddSingleton<IMauiElementEventBinder, MauiImageButtonEventsBinder>();
5760
services.TryAddSingleton<IMauiEventsBinder, MauiEventsBinder>();
5861

5962
services.AddSentry<SentryMauiOptions>();

test/Sentry.Maui.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,23 @@
99
}
1010
namespace Sentry.Maui
1111
{
12+
public class BreadcrumbEvent : System.IEquatable<Sentry.Maui.BreadcrumbEvent>
13+
{
14+
public BreadcrumbEvent(object? Sender, string EventName, [System.Runtime.CompilerServices.TupleElementNames(new string[] {
15+
"Key",
16+
"Value"})] params System.Collections.Generic.IEnumerable<System.ValueTuple<string, string>>[] ExtraData) { }
17+
public string EventName { get; init; }
18+
[System.Runtime.CompilerServices.TupleElementNames(new string[] {
19+
"Key",
20+
"Value"})]
21+
public System.Collections.Generic.IEnumerable<System.ValueTuple<string, string>>[] ExtraData { get; init; }
22+
public object? Sender { get; init; }
23+
}
24+
public interface IMauiElementEventBinder
25+
{
26+
void Bind(Microsoft.Maui.Controls.VisualElement element, System.Action<Sentry.Maui.BreadcrumbEvent> addBreadcrumb);
27+
void UnBind(Microsoft.Maui.Controls.VisualElement element);
28+
}
1229
public class SentryMauiOptions : Sentry.Extensions.Logging.SentryLoggingOptions
1330
{
1431
public SentryMauiOptions() { }
@@ -19,4 +36,19 @@ namespace Sentry.Maui
1936
public bool IncludeTitleInBreadcrumbs { get; set; }
2037
public void SetBeforeScreenshotCapture(System.Func<Sentry.SentryEvent, Sentry.SentryHint, bool> beforeCapture) { }
2138
}
39+
}
40+
namespace Sentry.Maui.Internal
41+
{
42+
public class MauiButtonEventsBinder : Sentry.Maui.IMauiElementEventBinder
43+
{
44+
public MauiButtonEventsBinder() { }
45+
public void Bind(Microsoft.Maui.Controls.VisualElement element, System.Action<Sentry.Maui.BreadcrumbEvent> addBreadcrumb) { }
46+
public void UnBind(Microsoft.Maui.Controls.VisualElement element) { }
47+
}
48+
public class MauiImageButtonEventsBinder : Sentry.Maui.IMauiElementEventBinder
49+
{
50+
public MauiImageButtonEventsBinder() { }
51+
public void Bind(Microsoft.Maui.Controls.VisualElement element, System.Action<Sentry.Maui.BreadcrumbEvent> addBreadcrumb) { }
52+
public void UnBind(Microsoft.Maui.Controls.VisualElement element) { }
53+
}
2254
}

test/Sentry.Maui.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,23 @@
99
}
1010
namespace Sentry.Maui
1111
{
12+
public class BreadcrumbEvent : System.IEquatable<Sentry.Maui.BreadcrumbEvent>
13+
{
14+
public BreadcrumbEvent(object? Sender, string EventName, [System.Runtime.CompilerServices.TupleElementNames(new string[] {
15+
"Key",
16+
"Value"})] params System.Collections.Generic.IEnumerable<System.ValueTuple<string, string>>[] ExtraData) { }
17+
public string EventName { get; init; }
18+
[System.Runtime.CompilerServices.TupleElementNames(new string[] {
19+
"Key",
20+
"Value"})]
21+
public System.Collections.Generic.IEnumerable<System.ValueTuple<string, string>>[] ExtraData { get; init; }
22+
public object? Sender { get; init; }
23+
}
24+
public interface IMauiElementEventBinder
25+
{
26+
void Bind(Microsoft.Maui.Controls.VisualElement element, System.Action<Sentry.Maui.BreadcrumbEvent> addBreadcrumb);
27+
void UnBind(Microsoft.Maui.Controls.VisualElement element);
28+
}
1229
public class SentryMauiOptions : Sentry.Extensions.Logging.SentryLoggingOptions
1330
{
1431
public SentryMauiOptions() { }
@@ -19,4 +36,19 @@ namespace Sentry.Maui
1936
public bool IncludeTitleInBreadcrumbs { get; set; }
2037
public void SetBeforeScreenshotCapture(System.Func<Sentry.SentryEvent, Sentry.SentryHint, bool> beforeCapture) { }
2138
}
39+
}
40+
namespace Sentry.Maui.Internal
41+
{
42+
public class MauiButtonEventsBinder : Sentry.Maui.IMauiElementEventBinder
43+
{
44+
public MauiButtonEventsBinder() { }
45+
public void Bind(Microsoft.Maui.Controls.VisualElement element, System.Action<Sentry.Maui.BreadcrumbEvent> addBreadcrumb) { }
46+
public void UnBind(Microsoft.Maui.Controls.VisualElement element) { }
47+
}
48+
public class MauiImageButtonEventsBinder : Sentry.Maui.IMauiElementEventBinder
49+
{
50+
public MauiImageButtonEventsBinder() { }
51+
public void Bind(Microsoft.Maui.Controls.VisualElement element, System.Action<Sentry.Maui.BreadcrumbEvent> addBreadcrumb) { }
52+
public void UnBind(Microsoft.Maui.Controls.VisualElement element) { }
53+
}
2254
}

test/Sentry.Maui.Tests/MauiEventsBinderTests.Button.cs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@ public void Button_CommonEvents_AddsBreadcrumb(string eventName)
1515
{
1616
StyleId = "button"
1717
};
18-
_fixture.Binder.HandleButtonEvents(button);
18+
var el = new ElementEventArgs(button);
19+
_fixture.Binder.OnApplicationOnDescendantAdded(null, el);
1920

2021
// Act
2122
button.RaiseEvent(eventName, EventArgs.Empty);
@@ -40,11 +41,13 @@ public void Button_UnbindCommonEvents_DoesNotAddBreadcrumb(string eventName)
4041
{
4142
StyleId = "button"
4243
};
43-
_fixture.Binder.HandleButtonEvents(button);
44+
var el = new ElementEventArgs(button);
45+
_fixture.Binder.OnApplicationOnDescendantAdded(null, el);
46+
4447
button.RaiseEvent(eventName, EventArgs.Empty);
4548
Assert.Single(_fixture.Scope.Breadcrumbs); // Sanity check
4649

47-
_fixture.Binder.HandleButtonEvents(button, bind: false);
50+
_fixture.Binder.OnApplicationOnDescendantRemoved(null, el);
4851

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

0 commit comments

Comments
 (0)