Skip to content
Merged
Show file tree
Hide file tree
Changes from 16 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 applies Session Replay masks to specific controls 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(SentryMauiOptions options)
{
_options = options.Value;
_options = options;
#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
115 changes: 115 additions & 0 deletions test/Sentry.Maui.Tests/MauiCustomSessionReplayMaskBinderTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
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;
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 binder = new MauiSessionReplayMaskControlsOfTypeBinder(options);

// 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 binder = new MauiSessionReplayMaskControlsOfTypeBinder(options);

// 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 binder = new MauiSessionReplayMaskControlsOfTypeBinder(options);

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

}
34 changes: 34 additions & 0 deletions test/Sentry.Maui.Tests/MauiEventsBinderTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,38 @@ 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 enabledBinder = new MauiSessionReplayMaskControlsOfTypeBinder(options1);

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

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