diff --git a/CHANGELOG.md b/CHANGELOG.md index 12ae447ec3..c9627550f3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,8 @@ ### Fixes - The HTTP instrumentation uses the span created for the outgoing request in the sentry-trace header, fixing the parent-child relationship between client and server ([#4264](https://github.com/getsentry/sentry-dotnet/pull/4264)) +- ExtraData not captured for Breadcrumbs in MauiEventsBinder ([#4254](https://github.com/getsentry/sentry-dotnet/pull/4254)) + - NOTE: Required breaking changes to the public API of `Sentry.Maui.BreadcrumbEvent`, while keeping an _Obsolete_ constructor for backward compatibility. - InvalidOperationException sending attachments on Android with LLVM enabled ([#4276](https://github.com/getsentry/sentry-dotnet/pull/4276)) ### Dependencies diff --git a/src/Sentry.Maui/BreadcrumbEvent.cs b/src/Sentry.Maui/BreadcrumbEvent.cs new file mode 100644 index 0000000000..48a9252901 --- /dev/null +++ b/src/Sentry.Maui/BreadcrumbEvent.cs @@ -0,0 +1,71 @@ +namespace Sentry.Maui; + +/// +/// Argument to the OnBreadcrumbCreateCallback +/// +public sealed class BreadcrumbEvent +{ + /// + /// The sender of the event, usually the control that triggered it. + /// + public object? Sender { get; } + + /// + /// The event name (e.g. "Tapped", "Swiped", etc.) + /// + public string EventName { get; } + + /// + /// Any extra data to be included in the breadcrumb. This would typically be event specific information (for example + /// it could include the X, Y coordinates of a tap event). + /// + public IEnumerable> ExtraData { get; } + + /// + /// Creates a new BreadcrumbEvent + /// + public BreadcrumbEvent(object? sender, string eventName) + : this(sender, eventName, Array.Empty>()) + { + } + + /// + /// Creates a new BreadcrumbEvent + /// + public BreadcrumbEvent( + object? sender, + string eventName, + params IEnumerable> extraData) + { + Sender = sender; + EventName = eventName; + ExtraData = extraData; + } + + /// + /// Creates a new BreadcrumbEvent + /// + public BreadcrumbEvent( + object? sender, + string eventName, + params IEnumerable<(string key, string value)> extraData) : this(sender, eventName, extraData.Select( + e => new KeyValuePair(e.key, e.value))) + { + } + + /// + /// This constructor remains for backward compatibility. + /// + /// + /// + /// + [Obsolete("Use one of the other simpler constructors instead.")] + public BreadcrumbEvent( + object? sender, + string eventName, + IEnumerable<(string Key, string Value)>[] extraData) : this(sender, eventName, extraData.SelectMany( + x => x.Select(pair => new KeyValuePair(pair.Key, pair.Value))) + ) + { + } +} diff --git a/src/Sentry.Maui/IMauiElementEventBinder.cs b/src/Sentry.Maui/IMauiElementEventBinder.cs index fcb9d1a1cc..d2a41cabd7 100644 --- a/src/Sentry.Maui/IMauiElementEventBinder.cs +++ b/src/Sentry.Maui/IMauiElementEventBinder.cs @@ -21,15 +21,3 @@ public interface IMauiElementEventBinder /// public void UnBind(VisualElement element); } - -/// -/// Breadcrumb arguments -/// -/// -/// -/// -public record BreadcrumbEvent( - object? Sender, - string EventName, - params IEnumerable<(string Key, string Value)>[] ExtraData -); diff --git a/src/Sentry.Maui/Internal/MauiButtonEventsBinder.cs b/src/Sentry.Maui/Internal/MauiButtonEventsBinder.cs index b938fdcb19..a8fe7fe85f 100644 --- a/src/Sentry.Maui/Internal/MauiButtonEventsBinder.cs +++ b/src/Sentry.Maui/Internal/MauiButtonEventsBinder.cs @@ -3,12 +3,12 @@ namespace Sentry.Maui.Internal; /// public class MauiButtonEventsBinder : IMauiElementEventBinder { - private Action? addBreadcrumbCallback; + private Action? _addBreadcrumbCallback; /// public void Bind(VisualElement element, Action addBreadcrumb) { - addBreadcrumbCallback = addBreadcrumb; + _addBreadcrumbCallback = addBreadcrumb; if (element is Button button) { @@ -30,11 +30,11 @@ public void UnBind(VisualElement element) } private void OnButtonOnClicked(object? sender, EventArgs _) - => addBreadcrumbCallback?.Invoke(new(sender, nameof(Button.Clicked))); + => _addBreadcrumbCallback?.Invoke(new(sender, nameof(Button.Clicked))); private void OnButtonOnPressed(object? sender, EventArgs _) - => addBreadcrumbCallback?.Invoke(new(sender, nameof(Button.Pressed))); + => _addBreadcrumbCallback?.Invoke(new(sender, nameof(Button.Pressed))); private void OnButtonOnReleased(object? sender, EventArgs _) - => addBreadcrumbCallback?.Invoke(new(sender, nameof(Button.Released))); + => _addBreadcrumbCallback?.Invoke(new(sender, nameof(Button.Released))); } diff --git a/src/Sentry.Maui/Internal/MauiEventsBinder.cs b/src/Sentry.Maui/Internal/MauiEventsBinder.cs index dd35058015..737902041a 100644 --- a/src/Sentry.Maui/Internal/MauiEventsBinder.cs +++ b/src/Sentry.Maui/Internal/MauiEventsBinder.cs @@ -132,7 +132,14 @@ internal void OnBreadcrumbCreateCallback(BreadcrumbEvent breadcrumb) breadcrumb.Sender, breadcrumb.EventName, UserType, - UserActionCategory + UserActionCategory, + extra => + { + foreach (var (key, value) in breadcrumb.ExtraData) + { + extra[key] = value; + } + } ); } diff --git a/src/Sentry.Maui/Internal/MauiGestureRecognizerEventsBinder.cs b/src/Sentry.Maui/Internal/MauiGestureRecognizerEventsBinder.cs index f15a4d303a..f9c8f96c30 100644 --- a/src/Sentry.Maui/Internal/MauiGestureRecognizerEventsBinder.cs +++ b/src/Sentry.Maui/Internal/MauiGestureRecognizerEventsBinder.cs @@ -139,7 +139,7 @@ private static void OnPointerEnteredGesture(object? sender, PointerEventArgs e) ToPointerData(e) )); - private static IEnumerable<(string Key, string Value)> ToPointerData(PointerEventArgs e) => + private static (string Key, string Value)[] ToPointerData(PointerEventArgs e) => [ #if ANDROID ("MotionEventAction", e.PlatformArgs?.MotionEvent.Action.ToString() ?? string.Empty) diff --git a/test/Sentry.Maui.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt b/test/Sentry.Maui.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt index f608570b94..05a2136d62 100644 --- a/test/Sentry.Maui.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt +++ b/test/Sentry.Maui.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt @@ -11,17 +11,20 @@ namespace Microsoft.Maui.Hosting } namespace Sentry.Maui { - public class BreadcrumbEvent : System.IEquatable + public sealed class BreadcrumbEvent { - public BreadcrumbEvent(object? Sender, string EventName, [System.Runtime.CompilerServices.TupleElementNames(new string[] { + public BreadcrumbEvent(object? sender, string eventName) { } + public BreadcrumbEvent(object? sender, string eventName, System.Collections.Generic.IEnumerable> extraData) { } + public BreadcrumbEvent(object? sender, string eventName, [System.Runtime.CompilerServices.TupleElementNames(new string[] { + "key", + "value"})] System.Collections.Generic.IEnumerable> extraData) { } + [System.Obsolete("Use one of the other simpler constructors instead.")] + public BreadcrumbEvent(object? sender, string eventName, [System.Runtime.CompilerServices.TupleElementNames(new string[] { "Key", - "Value"})] params System.Collections.Generic.IEnumerable>[] ExtraData) { } - public string EventName { get; init; } - [System.Runtime.CompilerServices.TupleElementNames(new string[] { - "Key", - "Value"})] - public System.Collections.Generic.IEnumerable>[] ExtraData { get; init; } - public object? Sender { get; init; } + "Value"})] System.Collections.Generic.IEnumerable>[] extraData) { } + public string EventName { get; } + public System.Collections.Generic.IEnumerable> ExtraData { get; } + public object? Sender { get; } } public interface IMauiElementEventBinder { diff --git a/test/Sentry.Maui.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt b/test/Sentry.Maui.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt index f608570b94..12ae9dcc31 100644 --- a/test/Sentry.Maui.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt +++ b/test/Sentry.Maui.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt @@ -11,17 +11,20 @@ namespace Microsoft.Maui.Hosting } namespace Sentry.Maui { - public class BreadcrumbEvent : System.IEquatable + public sealed class BreadcrumbEvent { - public BreadcrumbEvent(object? Sender, string EventName, [System.Runtime.CompilerServices.TupleElementNames(new string[] { + public BreadcrumbEvent(object? sender, string eventName) { } + public BreadcrumbEvent(object? sender, string eventName, [System.Runtime.CompilerServices.ParamCollection] System.Collections.Generic.IEnumerable> extraData) { } + public BreadcrumbEvent(object? sender, string eventName, [System.Runtime.CompilerServices.ParamCollection] [System.Runtime.CompilerServices.TupleElementNames(new string[] { + "key", + "value"})] System.Collections.Generic.IEnumerable> extraData) { } + [System.Obsolete("Use one of the other simpler constructors instead.")] + public BreadcrumbEvent(object? sender, string eventName, [System.Runtime.CompilerServices.TupleElementNames(new string[] { "Key", - "Value"})] params System.Collections.Generic.IEnumerable>[] ExtraData) { } - public string EventName { get; init; } - [System.Runtime.CompilerServices.TupleElementNames(new string[] { - "Key", - "Value"})] - public System.Collections.Generic.IEnumerable>[] ExtraData { get; init; } - public object? Sender { get; init; } + "Value"})] System.Collections.Generic.IEnumerable>[] extraData) { } + public string EventName { get; } + public System.Collections.Generic.IEnumerable> ExtraData { get; } + public object? Sender { get; } } public interface IMauiElementEventBinder { diff --git a/test/Sentry.Maui.Tests/BreadcrumbEventTests.cs b/test/Sentry.Maui.Tests/BreadcrumbEventTests.cs new file mode 100644 index 0000000000..c92938adde --- /dev/null +++ b/test/Sentry.Maui.Tests/BreadcrumbEventTests.cs @@ -0,0 +1,28 @@ +using System.Collections.Generic; +using FluentAssertions; +using Xunit; + +namespace Sentry.Maui.Tests; + +public class BreadcrumbEventTests +{ + [Fact] + public void BreadcrumbEvent_OldConstructor_EquivalentToNewConstructor() + { + // Arrange + var sender = new object(); + var eventName = "TestEvent"; + + // Act + IEnumerable<(string Key, string Value)>[] extraData = [[("key1", "value1")], [("key2", "value2")]]; +#pragma warning disable CS0618 // Type or member is obsolete + var oldEvent = new BreadcrumbEvent(sender, eventName, extraData); +#pragma warning restore CS0618 // Type or member is obsolete + var newEvent = new BreadcrumbEvent(sender, eventName, ("key1", "value1"), ("key2", "value2")); + + // Assert + oldEvent.Sender.Should().Be(newEvent.Sender); + oldEvent.EventName.Should().Be(newEvent.EventName); + oldEvent.ExtraData.Should().BeEquivalentTo(newEvent.ExtraData); + } +} diff --git a/test/Sentry.Maui.Tests/MauiEventsBinderTests.Button.cs b/test/Sentry.Maui.Tests/MauiButtonEventsBinderTests.cs similarity index 92% rename from test/Sentry.Maui.Tests/MauiEventsBinderTests.Button.cs rename to test/Sentry.Maui.Tests/MauiButtonEventsBinderTests.cs index 9fe7e2922e..aedaf9cd3a 100644 --- a/test/Sentry.Maui.Tests/MauiEventsBinderTests.Button.cs +++ b/test/Sentry.Maui.Tests/MauiButtonEventsBinderTests.cs @@ -2,8 +2,10 @@ namespace Sentry.Maui.Tests; -public partial class MauiEventsBinderTests +public class MauiButtonEventsBinderTests { + private readonly MauiEventsBinderFixture _fixture = new(new MauiButtonEventsBinder()); + [Theory] [InlineData(nameof(Button.Clicked))] [InlineData(nameof(Button.Pressed))] diff --git a/test/Sentry.Maui.Tests/MauiEventsBinderFixture.cs b/test/Sentry.Maui.Tests/MauiEventsBinderFixture.cs new file mode 100644 index 0000000000..38cb143a7a --- /dev/null +++ b/test/Sentry.Maui.Tests/MauiEventsBinderFixture.cs @@ -0,0 +1,33 @@ +using Sentry.Maui.Internal; + +namespace Sentry.Maui.Tests; + +internal class MauiEventsBinderFixture +{ + public IHub Hub { get; } + + public MauiEventsBinder Binder { get; } + + public Scope Scope { get; } = new(); + + public SentryMauiOptions Options { get; } = new(); + + public MauiEventsBinderFixture(params IEnumerable elementEventBinders) + { + Hub = Substitute.For(); + Hub.SubstituteConfigureScope(Scope); + + Scope.Transaction = Substitute.For(); + + Options.Debug = true; + var logger = Substitute.For(); + logger.IsEnabled(Arg.Any()).Returns(true); + Options.DiagnosticLogger = logger; + var options = Microsoft.Extensions.Options.Options.Create(Options); + Binder = new MauiEventsBinder( + Hub, + options, + elementEventBinders + ); + } +} diff --git a/test/Sentry.Maui.Tests/MauiEventsBinderTests.cs b/test/Sentry.Maui.Tests/MauiEventsBinderTests.cs index 9b050b8bdb..0de536dcfd 100644 --- a/test/Sentry.Maui.Tests/MauiEventsBinderTests.cs +++ b/test/Sentry.Maui.Tests/MauiEventsBinderTests.cs @@ -4,41 +4,35 @@ namespace Sentry.Maui.Tests; public partial class MauiEventsBinderTests { - private class Fixture - { - public IHub Hub { get; } + private readonly MauiEventsBinderFixture _fixture = new(); - public MauiEventsBinder Binder { get; } + // Most of the tests for this class are in separate partial class files for better organisation - public Scope Scope { get; } = new(); + [Fact] + public void OnBreadcrumbCreateCallback_CreatesBreadcrumb() + { + // Arrange + var breadcrumbEvent = new BreadcrumbEvent(new object(), "TestName", + ("key1", "value1"), ("key2", "value2") + ); - public SentryMauiOptions Options { get; } = new(); + // Act + _fixture.Binder.OnBreadcrumbCreateCallback(breadcrumbEvent); - public Fixture() + // Assert + using (new AssertionScope()) { - Hub = Substitute.For(); - Hub.SubstituteConfigureScope(Scope); - - Scope.Transaction = Substitute.For(); - - Options.Debug = true; - var logger = Substitute.For(); - logger.IsEnabled(Arg.Any()).Returns(true); - Options.DiagnosticLogger = logger; - var options = Microsoft.Extensions.Options.Options.Create(Options); - Binder = new MauiEventsBinder( - Hub, - options, - [ - new MauiButtonEventsBinder(), - new MauiImageButtonEventsBinder(), - new MauiGestureRecognizerEventsBinder() - ] - ); + var crumb = Assert.Single(_fixture.Scope.Breadcrumbs); + Assert.Equal("Object.TestName", crumb.Message); + Assert.Equal(BreadcrumbLevel.Info, crumb.Level); + Assert.Equal(MauiEventsBinder.UserType, crumb.Type); + Assert.Equal(MauiEventsBinder.UserActionCategory, crumb.Category); + Assert.NotNull(crumb.Data); + Assert.Equal(breadcrumbEvent.ExtraData.Count(), crumb.Data.Count); + foreach (var (key, value) in breadcrumbEvent.ExtraData) + { + crumb.Data.Should().Contain(kvp => kvp.Key == key && kvp.Value == value); + } } } - - private readonly Fixture _fixture = new(); - - // Tests are in partial class files for better organization } diff --git a/test/Sentry.Maui.Tests/MauiEventsBinderTests.GestureRecognizers.cs b/test/Sentry.Maui.Tests/MauiGestureRecognizerEventsBinderTests.cs similarity index 96% rename from test/Sentry.Maui.Tests/MauiEventsBinderTests.GestureRecognizers.cs rename to test/Sentry.Maui.Tests/MauiGestureRecognizerEventsBinderTests.cs index f9d8cefe45..9e81385ca2 100644 --- a/test/Sentry.Maui.Tests/MauiEventsBinderTests.GestureRecognizers.cs +++ b/test/Sentry.Maui.Tests/MauiGestureRecognizerEventsBinderTests.cs @@ -2,8 +2,10 @@ namespace Sentry.Maui.Tests; -public partial class MauiEventsBinderTests +public class MauiGestureRecognizerEventsBinderTests { + private readonly MauiEventsBinderFixture _fixture = new(new MauiGestureRecognizerEventsBinder()); + [SkippableFact] public void TapGestureRecognizer_LifecycleEvents_AddsBreadcrumb() { diff --git a/test/Sentry.Maui.Tests/MauiEventsBinderTests.ImageButton.cs b/test/Sentry.Maui.Tests/MauiImageButtonEventsBinderTests.cs similarity index 92% rename from test/Sentry.Maui.Tests/MauiEventsBinderTests.ImageButton.cs rename to test/Sentry.Maui.Tests/MauiImageButtonEventsBinderTests.cs index 83f9d4aab5..f4b1360e9e 100644 --- a/test/Sentry.Maui.Tests/MauiEventsBinderTests.ImageButton.cs +++ b/test/Sentry.Maui.Tests/MauiImageButtonEventsBinderTests.cs @@ -2,8 +2,10 @@ namespace Sentry.Maui.Tests; -public partial class MauiEventsBinderTests +public class MauiImageButtonEventsBinderTests { + private readonly MauiEventsBinderFixture _fixture = new(new MauiImageButtonEventsBinder()); + [Theory] [InlineData(nameof(ImageButton.Clicked))] [InlineData(nameof(ImageButton.Pressed))]