Skip to content

Commit 282b5c6

Browse files
Only register MauiSessionReplayMaskControlsOfTypeBinder when relevant (#4445)
Resolves #4439
1 parent af44ea2 commit 282b5c6

File tree

9 files changed

+202
-70
lines changed

9 files changed

+202
-70
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
- Ensure all buffered logs are sent to Sentry when the application terminates unexpectedly ([#4425](https://github.com/getsentry/sentry-dotnet/pull/4425))
1616
- `InvalidOperationException` potentially thrown during a race condition, especially in concurrent high-volume logging scenarios ([#4428](https://github.com/getsentry/sentry-dotnet/pull/4428))
1717
- Blocking calls are no longer treated as unhandled crashes ([#4458](https://github.com/getsentry/sentry-dotnet/pull/4458))
18+
- 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))
1819

1920
### Dependencies
2021

samples/Sentry.Samples.Maui/MauiProgram.cs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,15 +45,18 @@ public static MauiApp CreateMauiApp()
4545
options.AddCommunityToolkitIntegration();
4646

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

src/Sentry.Maui/Internal/MauiEventsBinder.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
11
using Microsoft.Extensions.Options;
2-
using Microsoft.Maui.Platform;
3-
using Sentry.Extensibility;
42

53
namespace Sentry.Maui.Internal;
64

@@ -13,7 +11,7 @@ internal class MauiEventsBinder : IMauiEventsBinder
1311
{
1412
private readonly IHub _hub;
1513
private readonly SentryMauiOptions _options;
16-
private readonly IEnumerable<IMauiElementEventBinder> _elementEventBinders;
14+
internal readonly IEnumerable<IMauiElementEventBinder> _elementEventBinders;
1715

1816
// https://develop.sentry.dev/sdk/event-payloads/breadcrumbs/#breadcrumb-types
1917
// https://github.com/getsentry/sentry/blob/master/static/app/types/breadcrumbs.tsx
@@ -29,7 +27,9 @@ public MauiEventsBinder(IHub hub, IOptions<SentryMauiOptions> options, IEnumerab
2927
{
3028
_hub = hub;
3129
_options = options.Value;
32-
_elementEventBinders = elementEventBinders;
30+
_elementEventBinders = elementEventBinders.Where(b
31+
=> b is not MauiSessionReplayMaskControlsOfTypeBinder maskControlTypeBinder
32+
|| maskControlTypeBinder.IsEnabled);
3333
}
3434

3535
public void HandleApplicationEvents(Application application, bool bind = true)

src/Sentry.Maui/Internal/MauiVisualElementEventsBinder.cs renamed to src/Sentry.Maui/Internal/MauiSessionReplayMaskControlsOfTypeBinder.cs

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,21 @@ namespace Sentry.Maui.Internal;
99
/// <summary>
1010
/// Masks or unmasks visual elements for session replay recordings
1111
/// </summary>
12-
internal class MauiVisualElementEventsBinder : IMauiElementEventBinder
12+
internal class MauiSessionReplayMaskControlsOfTypeBinder : IMauiElementEventBinder
1313
{
1414
private readonly SentryMauiOptions _options;
1515

16-
public MauiVisualElementEventsBinder(IOptions<SentryMauiOptions> options)
16+
internal bool IsEnabled { get; }
17+
18+
public MauiSessionReplayMaskControlsOfTypeBinder(IOptions<SentryMauiOptions> options)
1719
{
1820
_options = options.Value;
21+
#if __ANDROID__
22+
var replayOptions = _options.Native.ExperimentalOptions.SessionReplay;
23+
IsEnabled = replayOptions is { IsSessionReplayEnabled: true, IsTypeMaskingUsed: true };
24+
#else
25+
IsEnabled = false;
26+
#endif
1927
}
2028

2129
/// <inheritdoc />

src/Sentry.Maui/SentryMauiAppBuilderExtensions.cs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
using Microsoft.Extensions.Logging;
33
using Microsoft.Extensions.Options;
44
using Microsoft.Maui.LifecycleEvents;
5-
using Sentry;
65
using Sentry.Extensibility;
76
using Sentry.Extensions.Logging.Extensions.DependencyInjection;
87
using Sentry.Maui;
@@ -67,9 +66,9 @@ public static MauiAppBuilder UseSentry(this MauiAppBuilder builder,
6766
services.AddSingleton<IMauiElementEventBinder, MauiButtonEventsBinder>();
6867
services.AddSingleton<IMauiElementEventBinder, MauiImageButtonEventsBinder>();
6968
services.AddSingleton<IMauiElementEventBinder, MauiGestureRecognizerEventsBinder>();
70-
services.AddSingleton<IMauiElementEventBinder, MauiVisualElementEventsBinder>();
69+
services.AddSingleton<IMauiElementEventBinder, MauiSessionReplayMaskControlsOfTypeBinder>();
7170

72-
// Resolve the configured options and register any event binders that have been injected by integrations
71+
// Resolve options configured via the options callback and register any binders injected by integrations
7372
var options = new SentryMauiOptions();
7473
configureOptions?.Invoke(options);
7574
foreach (var eventBinder in options.IntegrationEventBinders)

src/Sentry/Platforms/Android/NativeOptions.cs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -272,18 +272,43 @@ public class NativeSentryReplayOptions
272272
public double? SessionSampleRate { get; set; }
273273
public bool MaskAllImages { get; set; } = true;
274274
public bool MaskAllText { get; set; } = true;
275+
275276
internal HashSet<Type> MaskedControls { get; } = [];
276277
internal HashSet<Type> UnmaskedControls { get; } = [];
277278

279+
internal bool IsSessionReplayEnabled => OnErrorSampleRate > 0.0 || SessionSampleRate > 0.0;
280+
281+
/// <summary>
282+
/// Allows you to mask all controls of a particular type for session replay recordings.
283+
/// </summary>
284+
/// <typeparam name="T">The Type of control that should be masked</typeparam>
285+
/// <remarks>
286+
/// WARNING: In apps with complex user interfaces, consisting of hundreds of visual controls on a single
287+
/// page, this option may cause performance issues. In such cases, consider applying SessionReplay.Mask
288+
/// attributes to individual controls instead:
289+
/// <code>sentry:SessionReplay.Mask="Mask"</code>
290+
/// </remarks>
278291
public void MaskControlsOfType<T>()
279292
{
280293
MaskedControls.Add(typeof(T));
281294
}
282295

296+
/// <summary>
297+
/// Allows you to unmask all controls of a particular type for session replay recordings.
298+
/// </summary>
299+
/// <typeparam name="T">The Type of control that should be unmasked</typeparam>
300+
/// <remarks>
301+
/// WARNING: In apps with complex user interfaces, consisting of hundreds of visual controls on a single
302+
/// page, this option may cause performance issues. In such cases, consider applying SessionReplay.Mask
303+
/// attributes to individual controls instead:
304+
/// <code>sentry:SessionReplay.Mask="Unmask"</code>
305+
/// </remarks>
283306
public void UnmaskControlsOfType<T>()
284307
{
285308
UnmaskedControls.Add(typeof(T));
286309
}
310+
311+
internal bool IsTypeMaskingUsed => MaskedControls.Count > 0 || UnmaskedControls.Count > 0;
287312
}
288313

289314
/// <summary>
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
using Sentry.Maui.Internal;
2+
using Sentry.Maui.Tests.Mocks;
3+
#if __ANDROID__
4+
using View = Android.Views.View;
5+
#endif
6+
7+
namespace Sentry.Maui.Tests;
8+
9+
public class MauiSessionReplayMaskControlsOfTypeBinderTests
10+
{
11+
private class Fixture
12+
{
13+
public MauiSessionReplayMaskControlsOfTypeBinder ControlsOfTypeBinder { get; }
14+
15+
public SentryMauiOptions Options { get; } = new();
16+
17+
public Fixture()
18+
{
19+
Options.Debug = true;
20+
var logger = Substitute.For<IDiagnosticLogger>();
21+
logger.IsEnabled(Arg.Any<SentryLevel>()).Returns(true);
22+
Options.DiagnosticLogger = logger;
23+
var options = Microsoft.Extensions.Options.Options.Create(Options);
24+
ControlsOfTypeBinder = new MauiSessionReplayMaskControlsOfTypeBinder(options);
25+
}
26+
}
27+
28+
private readonly Fixture _fixture = new();
29+
30+
[Fact]
31+
public void OnElementLoaded_SenderIsNotVisualElement_LogsDebugAndReturns()
32+
{
33+
// Arrange
34+
var element = new MockElement("element");
35+
36+
// Act
37+
_fixture.ControlsOfTypeBinder.OnElementLoaded(element, EventArgs.Empty);
38+
39+
// Assert
40+
_fixture.Options.DiagnosticLogger.Received(1).LogDebug("OnElementLoaded: sender is not a VisualElement");
41+
}
42+
43+
[Fact]
44+
public void OnElementLoaded_HandlerIsNull_LogsDebugAndReturns()
45+
{
46+
// Arrange
47+
var element = new MockVisualElement("element")
48+
{
49+
Handler = null
50+
};
51+
52+
// Act
53+
_fixture.ControlsOfTypeBinder.OnElementLoaded(element, EventArgs.Empty);
54+
55+
// Assert
56+
_fixture.Options.DiagnosticLogger.Received(1).LogDebug("OnElementLoaded: handler is null");
57+
}
58+
59+
#if __ANDROID__
60+
[Theory]
61+
[InlineData(0.0, 1.0)]
62+
[InlineData(1.0, 0.0)]
63+
[InlineData(1.0, 1.0)]
64+
public void SessionReplayEnabled_IsEnabled(
65+
double sessionSampleRate, double onErrorSampleRate)
66+
{
67+
// Arrange
68+
var options = new SentryMauiOptions { Dsn = ValidDsn };
69+
// force custom masking to be enabled
70+
options.Native.ExperimentalOptions.SessionReplay.MaskControlsOfType<object>();
71+
// One of the below has to be non-zero for session replay to be enabled
72+
options.Native.ExperimentalOptions.SessionReplay.SessionSampleRate = sessionSampleRate;
73+
options.Native.ExperimentalOptions.SessionReplay.OnErrorSampleRate = onErrorSampleRate;
74+
75+
// Act
76+
var iOptions = Microsoft.Extensions.Options.Options.Create(options);
77+
var binder = new MauiSessionReplayMaskControlsOfTypeBinder(iOptions);
78+
79+
// Assert
80+
binder.IsEnabled.Should().Be(true);
81+
}
82+
83+
[Fact]
84+
public void SessionReplayDisabled_IsNotEnabled()
85+
{
86+
// Arrange
87+
var options = new SentryMauiOptions { Dsn = ValidDsn };
88+
// force custom masking to be enabled
89+
options.Native.ExperimentalOptions.SessionReplay.MaskControlsOfType<object>();
90+
// No sessionSampleRate or onErrorSampleRate set... so should be disabled
91+
92+
// Act
93+
var iOptions = Microsoft.Extensions.Options.Options.Create(options);
94+
var binder = new MauiSessionReplayMaskControlsOfTypeBinder(iOptions);
95+
96+
// Assert
97+
binder.IsEnabled.Should().Be(false);
98+
}
99+
100+
[Fact]
101+
public void UseSentry_NoMaskedControls_DoesNotRegisterMauiVisualElementEventsBinder()
102+
{
103+
// Arrange
104+
var options = new SentryMauiOptions { Dsn = ValidDsn };
105+
options.Native.ExperimentalOptions.SessionReplay.OnErrorSampleRate = 1.0;
106+
options.Native.ExperimentalOptions.SessionReplay.SessionSampleRate = 1.0;
107+
// Not really necessary, but just to be explicit
108+
options.Native.ExperimentalOptions.SessionReplay.MaskedControls.Clear();
109+
110+
// Act
111+
var iOptions = Microsoft.Extensions.Options.Options.Create(options);
112+
var binder = new MauiSessionReplayMaskControlsOfTypeBinder(iOptions);
113+
114+
// Assert
115+
binder.IsEnabled.Should().Be(false);
116+
}
117+
#endif
118+
119+
}

test/Sentry.Maui.Tests/MauiEventsBinderTests.cs

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,4 +35,40 @@ public void OnBreadcrumbCreateCallback_CreatesBreadcrumb()
3535
}
3636
}
3737
}
38+
39+
[Fact]
40+
public void ElementEventBinders_EnabledOnly()
41+
{
42+
// Arrange
43+
var options1 = new SentryMauiOptions { Dsn = ValidDsn };
44+
#if __ANDROID__
45+
options1.Native.ExperimentalOptions.SessionReplay.MaskControlsOfType<object>(); // force masking to be enabled
46+
options1.Native.ExperimentalOptions.SessionReplay.SessionSampleRate = 1.0;
47+
options1.Native.ExperimentalOptions.SessionReplay.OnErrorSampleRate = 1.0;
48+
#endif
49+
var iOptions1 = Microsoft.Extensions.Options.Options.Create(options1);
50+
var enabledBinder = new MauiSessionReplayMaskControlsOfTypeBinder(iOptions1);
51+
52+
var options2 = new SentryMauiOptions { Dsn = ValidDsn };
53+
#if __ANDROID__
54+
options2.Native.ExperimentalOptions.SessionReplay.SessionSampleRate = 0.0;
55+
options2.Native.ExperimentalOptions.SessionReplay.OnErrorSampleRate = 0.0;
56+
#endif
57+
var iOptions2 = Microsoft.Extensions.Options.Options.Create(options2);
58+
var disabledBinder = new MauiSessionReplayMaskControlsOfTypeBinder(iOptions2);
59+
60+
var buttonEventBinder = new MauiButtonEventsBinder();
61+
62+
// Act
63+
var fixture = new MauiEventsBinderFixture(buttonEventBinder, enabledBinder, disabledBinder);
64+
65+
// Assert
66+
#if __ANDROID__
67+
var expectedBinders = new List<IMauiElementEventBinder> { buttonEventBinder, enabledBinder };
68+
#else
69+
// We only register MauiSessionReplayMaskControlsOfTypeBinder on platforms that support Session Replay
70+
var expectedBinders = new List<IMauiElementEventBinder> { buttonEventBinder };
71+
#endif
72+
fixture.Binder._elementEventBinders.Should().BeEquivalentTo(expectedBinders);
73+
}
3874
}

test/Sentry.Maui.Tests/MauiVisualElementEventsBinderTests.cs

Lines changed: 0 additions & 59 deletions
This file was deleted.

0 commit comments

Comments
 (0)