Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
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 @@ -14,6 +14,7 @@
- Ensure all buffered logs are sent to Sentry when the application terminates unexpectedly ([#4425](https://github.com/getsentry/sentry-dotnet/pull/4425))
- `InvalidOperationException` potentially thrown during a race condition, especially in concurrent high-volume logging scenarios ([#4428](https://github.com/getsentry/sentry-dotnet/pull/4428))
- Blocking calls are no longer treated as unhandled crashes ([#4458](https://github.com/getsentry/sentry-dotnet/pull/4458))
- Only apply Session Replay masks to specific control types when necessary to avoid performance issues in MAUI apps with complex UIs ([#4445](https://github.com/getsentry/sentry-dotnet/pull/4445))

### Dependencies

Expand Down
7 changes: 5 additions & 2 deletions samples/Sentry.Samples.Maui/MauiProgram.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,15 +45,18 @@ public static MauiApp CreateMauiApp()
options.AddCommunityToolkitIntegration();

#if __ANDROID__
// Currently experimental support is only available on Android
// Currently, experimental support is only available on Android
options.Native.ExperimentalOptions.SessionReplay.OnErrorSampleRate = 1.0;
options.Native.ExperimentalOptions.SessionReplay.SessionSampleRate = 1.0;
// Mask all images and text by default. This can be overridden for individual view elements via the
// sentry:SessionReplay.Mask XML attribute (see MainPage.xaml for an example)
options.Native.ExperimentalOptions.SessionReplay.MaskAllImages = true;
options.Native.ExperimentalOptions.SessionReplay.MaskAllText = true;
// Alternatively the masking behaviour for entire classes of VisualElements can be configured here as
// Alternatively, the masking behaviour for entire classes of VisualElements can be configured here as
// an exception to the default behaviour.
// WARNING: In apps with complex user interfaces, consisting of hundreds of visual controls on a single
// page, this option may cause performance issues. In such cases, consider applying the
// sentry:SessionReplay.Mask="Unmask" attribute to individual controls instead.
options.Native.ExperimentalOptions.SessionReplay.UnmaskControlsOfType<Button>();
#endif

Expand Down
8 changes: 4 additions & 4 deletions src/Sentry.Maui/Internal/MauiEventsBinder.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
using Microsoft.Extensions.Options;
using Microsoft.Maui.Platform;
using Sentry.Extensibility;

namespace Sentry.Maui.Internal;

Expand All @@ -13,7 +11,7 @@ internal class MauiEventsBinder : IMauiEventsBinder
{
private readonly IHub _hub;
private readonly SentryMauiOptions _options;
private readonly IEnumerable<IMauiElementEventBinder> _elementEventBinders;
internal 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 @@ -29,7 +27,9 @@ public MauiEventsBinder(IHub hub, IOptions<SentryMauiOptions> options, IEnumerab
{
_hub = hub;
_options = options.Value;
_elementEventBinders = elementEventBinders;
_elementEventBinders = elementEventBinders.Where(b
=> b is not MauiSessionReplayMaskControlsOfTypeBinder maskControlTypeBinder
|| maskControlTypeBinder.IsEnabled);
}

public void HandleApplicationEvents(Application application, bool bind = true)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,21 @@ namespace Sentry.Maui.Internal;
/// <summary>
/// Masks or unmasks visual elements for session replay recordings
/// </summary>
internal class MauiVisualElementEventsBinder : IMauiElementEventBinder
internal class MauiSessionReplayMaskControlsOfTypeBinder : IMauiElementEventBinder
{
private readonly SentryMauiOptions _options;

public MauiVisualElementEventsBinder(IOptions<SentryMauiOptions> options)
internal bool IsEnabled { get; }

public MauiSessionReplayMaskControlsOfTypeBinder(IOptions<SentryMauiOptions> options)
{
_options = options.Value;
#if __ANDROID__
var replayOptions = _options.Native.ExperimentalOptions.SessionReplay;
IsEnabled = replayOptions is { IsSessionReplayEnabled: true, IsTypeMaskingUsed: true };
#else
IsEnabled = false;
#endif
}

/// <inheritdoc />
Expand Down
5 changes: 2 additions & 3 deletions src/Sentry.Maui/SentryMauiAppBuilderExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Microsoft.Maui.LifecycleEvents;
using Sentry;
using Sentry.Extensibility;
using Sentry.Extensions.Logging.Extensions.DependencyInjection;
using Sentry.Maui;
Expand Down Expand Up @@ -67,9 +66,9 @@ public static MauiAppBuilder UseSentry(this MauiAppBuilder builder,
services.AddSingleton<IMauiElementEventBinder, MauiButtonEventsBinder>();
services.AddSingleton<IMauiElementEventBinder, MauiImageButtonEventsBinder>();
services.AddSingleton<IMauiElementEventBinder, MauiGestureRecognizerEventsBinder>();
services.AddSingleton<IMauiElementEventBinder, MauiVisualElementEventsBinder>();
services.AddSingleton<IMauiElementEventBinder, MauiSessionReplayMaskControlsOfTypeBinder>();

// Resolve the configured options and register any event binders that have been injected by integrations
// Resolve options configured via the options callback and register any binders injected by integrations
var options = new SentryMauiOptions();
configureOptions?.Invoke(options);
foreach (var eventBinder in options.IntegrationEventBinders)
Expand Down
25 changes: 25 additions & 0 deletions src/Sentry/Platforms/Android/NativeOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -272,18 +272,43 @@ public class NativeSentryReplayOptions
public double? SessionSampleRate { get; set; }
public bool MaskAllImages { get; set; } = true;
public bool MaskAllText { get; set; } = true;

internal HashSet<Type> MaskedControls { get; } = [];
internal HashSet<Type> UnmaskedControls { get; } = [];

internal bool IsSessionReplayEnabled => OnErrorSampleRate > 0.0 || SessionSampleRate > 0.0;

/// <summary>
/// Allows you to mask all controls of a particular type for session replay recordings.
/// </summary>
/// <typeparam name="T">The Type of control that should be masked</typeparam>
/// <remarks>
/// WARNING: In apps with complex user interfaces, consisting of hundreds of visual controls on a single
/// page, this option may cause performance issues. In such cases, consider applying SessionReplay.Mask
/// attributes to individual controls instead:
/// <code>sentry:SessionReplay.Mask="Mask"</code>
/// </remarks>
public void MaskControlsOfType<T>()
{
MaskedControls.Add(typeof(T));
}

/// <summary>
/// Allows you to unmask all controls of a particular type for session replay recordings.
/// </summary>
/// <typeparam name="T">The Type of control that should be unmasked</typeparam>
/// <remarks>
/// WARNING: In apps with complex user interfaces, consisting of hundreds of visual controls on a single
/// page, this option may cause performance issues. In such cases, consider applying SessionReplay.Mask
/// attributes to individual controls instead:
/// <code>sentry:SessionReplay.Mask="Unmask"</code>
/// </remarks>
public void UnmaskControlsOfType<T>()
{
UnmaskedControls.Add(typeof(T));
}

internal bool IsTypeMaskingUsed => MaskedControls.Count > 0 || UnmaskedControls.Count > 0;
}

/// <summary>
Expand Down
119 changes: 119 additions & 0 deletions test/Sentry.Maui.Tests/MauiCustomSessionReplayMaskBinderTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
using Sentry.Maui.Internal;
using Sentry.Maui.Tests.Mocks;
#if __ANDROID__
using View = Android.Views.View;
#endif

namespace Sentry.Maui.Tests;

public class MauiSessionReplayMaskControlsOfTypeBinderTests
{
private class Fixture
{
public MauiSessionReplayMaskControlsOfTypeBinder ControlsOfTypeBinder { get; }

public SentryMauiOptions Options { get; } = new();

public Fixture()
{
Options.Debug = true;
var logger = Substitute.For<IDiagnosticLogger>();
logger.IsEnabled(Arg.Any<SentryLevel>()).Returns(true);
Options.DiagnosticLogger = logger;
var options = Microsoft.Extensions.Options.Options.Create(Options);
ControlsOfTypeBinder = new MauiSessionReplayMaskControlsOfTypeBinder(options);
}
}

private readonly Fixture _fixture = new();

[Fact]
public void OnElementLoaded_SenderIsNotVisualElement_LogsDebugAndReturns()
{
// Arrange
var element = new MockElement("element");

// Act
_fixture.ControlsOfTypeBinder.OnElementLoaded(element, EventArgs.Empty);

// Assert
_fixture.Options.DiagnosticLogger.Received(1).LogDebug("OnElementLoaded: sender is not a VisualElement");
}

[Fact]
public void OnElementLoaded_HandlerIsNull_LogsDebugAndReturns()
{
// Arrange
var element = new MockVisualElement("element")
{
Handler = null
};

// Act
_fixture.ControlsOfTypeBinder.OnElementLoaded(element, EventArgs.Empty);

// Assert
_fixture.Options.DiagnosticLogger.Received(1).LogDebug("OnElementLoaded: handler is null");
}

#if __ANDROID__
[Theory]
[InlineData(0.0, 1.0)]
[InlineData(1.0, 0.0)]
[InlineData(1.0, 1.0)]
public void SessionReplayEnabled_IsEnabled(
double sessionSampleRate, double onErrorSampleRate)
{
// Arrange
var options = new SentryMauiOptions { Dsn = ValidDsn };
// force custom masking to be enabled
options.Native.ExperimentalOptions.SessionReplay.MaskControlsOfType<object>();
// One of the below has to be non-zero for session replay to be enabled
options.Native.ExperimentalOptions.SessionReplay.SessionSampleRate = sessionSampleRate;
options.Native.ExperimentalOptions.SessionReplay.OnErrorSampleRate = onErrorSampleRate;

// Act
var iOptions = Microsoft.Extensions.Options.Options.Create(options);
var binder = new MauiSessionReplayMaskControlsOfTypeBinder(iOptions);

// Assert
binder.IsEnabled.Should().Be(true);
}

[Fact]
public void SessionReplayDisabled_IsNotEnabled()
{
// Arrange
var options = new SentryMauiOptions { Dsn = ValidDsn };
// force custom masking to be enabled
options.Native.ExperimentalOptions.SessionReplay.MaskControlsOfType<object>();
// No sessionSampleRate or onErrorSampleRate set... so should be disabled

// Act
var iOptions = Microsoft.Extensions.Options.Options.Create(options);
var binder = new MauiSessionReplayMaskControlsOfTypeBinder(iOptions);

// Assert
binder.IsEnabled.Should().Be(false);
}

[Fact]
public void UseSentry_NoMaskedControls_DoesNotRegisterMauiVisualElementEventsBinder()
{
// Arrange
var options = new SentryMauiOptions { Dsn = ValidDsn };
options.Native.ExperimentalOptions.SessionReplay.OnErrorSampleRate = 1.0;
options.Native.ExperimentalOptions.SessionReplay.SessionSampleRate = 1.0;
// Not really necessary, but just to be explicit
options.Native.ExperimentalOptions.SessionReplay.MaskedControls.Clear();

// Act
var iOptions = Microsoft.Extensions.Options.Options.Create(options);
var binder = new MauiSessionReplayMaskControlsOfTypeBinder(iOptions);

// Assert
binder.IsEnabled.Should().Be(false);
}
#endif

}
36 changes: 36 additions & 0 deletions test/Sentry.Maui.Tests/MauiEventsBinderTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,40 @@ public void OnBreadcrumbCreateCallback_CreatesBreadcrumb()
}
}
}

[Fact]
public void ElementEventBinders_EnabledOnly()
{
// Arrange
var options1 = new SentryMauiOptions { Dsn = ValidDsn };
#if __ANDROID__
options1.Native.ExperimentalOptions.SessionReplay.MaskControlsOfType<object>(); // force masking to be enabled
options1.Native.ExperimentalOptions.SessionReplay.SessionSampleRate = 1.0;
options1.Native.ExperimentalOptions.SessionReplay.OnErrorSampleRate = 1.0;
#endif
var iOptions1 = Microsoft.Extensions.Options.Options.Create(options1);
var enabledBinder = new MauiSessionReplayMaskControlsOfTypeBinder(iOptions1);

var options2 = new SentryMauiOptions { Dsn = ValidDsn };
#if __ANDROID__
options2.Native.ExperimentalOptions.SessionReplay.SessionSampleRate = 0.0;
options2.Native.ExperimentalOptions.SessionReplay.OnErrorSampleRate = 0.0;
#endif
var iOptions2 = Microsoft.Extensions.Options.Options.Create(options2);
var disabledBinder = new MauiSessionReplayMaskControlsOfTypeBinder(iOptions2);

var buttonEventBinder = new MauiButtonEventsBinder();

// Act
var fixture = new MauiEventsBinderFixture(buttonEventBinder, enabledBinder, disabledBinder);

// Assert
#if __ANDROID__
var expectedBinders = new List<IMauiElementEventBinder> { buttonEventBinder, enabledBinder };
#else
// We only register MauiSessionReplayMaskControlsOfTypeBinder on platforms that support Session Replay
var expectedBinders = new List<IMauiElementEventBinder> { buttonEventBinder };
#endif
fixture.Binder._elementEventBinders.Should().BeEquivalentTo(expectedBinders);
}
}
59 changes: 0 additions & 59 deletions test/Sentry.Maui.Tests/MauiVisualElementEventsBinderTests.cs

This file was deleted.

Loading