Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Changelog

## Unreleased

### Features

- Custom SessionReplay masks in MAUI Android apps ([#4121](https://github.com/getsentry/sentry-dotnet/pull/4121))

## 5.6.0

### Features
Expand Down
2 changes: 2 additions & 0 deletions samples/Sentry.Samples.Maui/MainPage.xaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:sentry="http://schemas.sentry.io/maui"
x:Class="Sentry.Samples.Maui.MainPage">

<ScrollView>
Expand All @@ -11,6 +12,7 @@

<Image
Source="dotnet_bot.png"
sentry:SessionReplay.Mask="Unmask"
SemanticProperties.Description="Cute dot net bot waving hi to you!"
HeightRequest="200"
HorizontalOptions="Center" />
Expand Down
11 changes: 8 additions & 3 deletions samples/Sentry.Samples.Maui/MauiProgram.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,17 @@ public static MauiApp CreateMauiApp()
options.Debug = true;
options.SampleRate = 1.0F;

#if ANDROID
#if __ANDROID__
// Currently experimental support is only available on Android
options.Native.ExperimentalOptions.SessionReplay.OnErrorSampleRate = 1.0;
options.Native.ExperimentalOptions.SessionReplay.SessionSampleRate = 1.0;
options.Native.ExperimentalOptions.SessionReplay.MaskAllImages = false;
options.Native.ExperimentalOptions.SessionReplay.MaskAllText = false;
// 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
// an exception to the default behaviour.
options.Native.ExperimentalOptions.SessionReplay.UnmaskControlsOfType<Button>();
#endif

options.SetBeforeScreenshotCapture((@event, hint) =>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
#if __ANDROID__
using Android.App;
using Android.Content.PM;
using Android.OS;
#endif

namespace Sentry.Samples.Maui;

Expand Down
3 changes: 3 additions & 0 deletions src/Sentry.Maui/AssemblyInfo.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// XML namespaces for custom XAML elements defined in the Sentry.Maui assembly (e.g. Bindable Properties)
[assembly: XmlnsDefinition("http://schemas.sentry.io/maui", "Sentry.Maui")]
[assembly: Microsoft.Maui.Controls.XmlnsPrefix("http://schemas.sentry.io/maui", "sentry")]
1 change: 0 additions & 1 deletion src/Sentry.Maui/Internal/MauiButtonEventsBinder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ public void UnBind(VisualElement element)
}
}


private void OnButtonOnClicked(object? sender, EventArgs _)
=> addBreadcrumbCallback?.Invoke(new(sender, nameof(Button.Clicked)));

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

namespace Sentry.Maui.Internal;

Expand Down
66 changes: 66 additions & 0 deletions src/Sentry.Maui/Internal/MauiVisualElementEventsBinder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
using Microsoft.Extensions.Options;
using Sentry.Extensibility;
#if __ANDROID__
using View = Android.Views.View;
#endif

namespace Sentry.Maui.Internal;

/// <summary>
/// Masks or unmasks visual elements for session replay recordings
/// </summary>
internal class MauiVisualElementEventsBinder : IMauiElementEventBinder
{
private readonly SentryMauiOptions _options;

public MauiVisualElementEventsBinder(IOptions<SentryMauiOptions> options)
{
_options = options.Value;
}

/// <inheritdoc />
public void Bind(VisualElement element, Action<BreadcrumbEvent> _)
{
element.Loaded += OnElementLoaded;
}

/// <inheritdoc />
public void UnBind(VisualElement element)
{
element.Loaded -= OnElementLoaded;
}

internal void OnElementLoaded(object? sender, EventArgs _)
{
if (sender is not VisualElement element)
{
_options.LogDebug("OnElementLoaded: sender is not a VisualElement");
return;
}

var handler = element.Handler;
if (handler is null)
{
_options.LogDebug("OnElementLoaded: element.Handler is null");
return;
}

#if __ANDROID__
if (element.Handler?.PlatformView is not View nativeView)
{
return;
}

if (_options.Native.ExperimentalOptions.SessionReplay.MaskedControls.FirstOrDefault(maskType => element.GetType().IsAssignableFrom(maskType)) is not null)
{
nativeView.Tag = SessionReplayMaskMode.Mask.ToNativeTag();
_options.LogDebug("OnElementLoaded: Successfully set sentry-mask tag on native view");
}
else if (_options.Native.ExperimentalOptions.SessionReplay.UnmaskedControls.FirstOrDefault(unmaskType => element.GetType().IsAssignableFrom(unmaskType)) is not null)
{
nativeView.Tag = SessionReplayMaskMode.Unmask.ToNativeTag();
_options.LogDebug("OnElementLoaded: Successfully set sentry-unmask tag on native view");
}
#endif
}
}
1 change: 1 addition & 0 deletions src/Sentry.Maui/SentryMauiAppBuilderExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ public static MauiAppBuilder UseSentry(this MauiAppBuilder builder,

services.AddSingleton<IMauiElementEventBinder, MauiButtonEventsBinder>();
services.AddSingleton<IMauiElementEventBinder, MauiImageButtonEventsBinder>();
services.AddSingleton<IMauiElementEventBinder, MauiVisualElementEventsBinder>();
services.TryAddSingleton<IMauiEventsBinder, MauiEventsBinder>();

services.AddSentry<SentryMauiOptions>();
Expand Down
2 changes: 1 addition & 1 deletion src/Sentry.Maui/SentryMauiOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ public SentryMauiOptions()
/// if this callback return false the capture will not take place
/// </remarks>
/// <code>
///
///
///options.SetBeforeCapture((@event, hint) =>
///{
/// // Return true to capture or false to prevent the capture
Expand Down
90 changes: 90 additions & 0 deletions src/Sentry.Maui/SessionReplay.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@

using Sentry.Infrastructure;
#if __ANDROID__
using View = Android.Views.View;
using Android.Views;
using Java.Lang;
using Microsoft.Maui.Handlers;
using Microsoft.Maui.Platform;
#endif

namespace Sentry.Maui;

/// <summary>
/// Contains custom <see cref="BindableProperty"/> definitions used to control the behaviour of the Sentry SessionReplay
/// feature in MAUI apps.
/// <remarks>
/// NOTE: Session Replay is currently an experimental feature for MAUI and is subject to change.
/// </remarks>
/// </summary>
public static class SessionReplay
{
/// <summary>
/// Mask can be used to either unmask or mask a view.
/// </summary>
public static readonly BindableProperty MaskProperty =
BindableProperty.CreateAttached(
"Mask",
typeof(SessionReplayMaskMode),
typeof(SessionReplay),
defaultValue: SessionReplayMaskMode.Mask,
propertyChanged: OnMaskChanged);

/// <summary>
/// Gets the value of the Mask property for a view.
/// </summary>
public static SessionReplayMaskMode GetMask(BindableObject view) => (SessionReplayMaskMode)view.GetValue(MaskProperty);

/// <summary>
/// Sets the value of the Mask property for a view.
/// </summary>
/// <param name="view">The view element to mask or unmask</param>
/// <param name="value">The value to assign. Can be either "sentry-mask" or "sentry-unmask".</param>
public static void SetMask(BindableObject view, SessionReplayMaskMode value) => view.SetValue(MaskProperty, value);

private static void OnMaskChanged(BindableObject bindable, object oldValue, object newValue)
{
#if __ANDROID__
if (bindable is not VisualElement ve || newValue is not SessionReplayMaskMode maskSetting)
{
return;
}

// This code looks pretty funky... just matching how funky MAUI is though.
// See https://github.com/getsentry/sentry-dotnet/pull/4121#discussion_r2054129378
ve.HandlerChanged -= OnMaskedElementHandlerChanged;
ve.HandlerChanged -= OnUnmaskedElementHandlerChanged;

if (maskSetting == SessionReplayMaskMode.Mask)
{
ve.HandlerChanged += OnMaskedElementHandlerChanged;
}
else if (maskSetting == SessionReplayMaskMode.Unmask)
{
ve.HandlerChanged += OnUnmaskedElementHandlerChanged;
}
#endif
}

#if __ANDROID__
private static void OnMaskedElementHandlerChanged(object? sender, EventArgs _)
{
if ((sender as VisualElement)?.Handler?.PlatformView is not View nativeView)
{
return;
}

nativeView.Tag = SessionReplayMaskMode.Mask.ToNativeTag();
}

private static void OnUnmaskedElementHandlerChanged(object? sender, EventArgs _)
{
if ((sender as VisualElement)?.Handler?.PlatformView is not View nativeView)
{
return;
}

nativeView.Tag = SessionReplayMaskMode.Unmask.ToNativeTag();
}
#endif
}
32 changes: 32 additions & 0 deletions src/Sentry.Maui/SessionReplayMaskMode.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
namespace Sentry.Maui;

/// <summary>
/// Controls the masking behaviour of the Session Replay feature.
/// </summary>
public enum SessionReplayMaskMode
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: Is the term "mask mode" (or "masking mode") established through other SDKs, or is this enum a .NET-only concept to "type-ify" the strings?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The later - I made it up to limit the number of possibilities presented by the tooling when writing XAML.

Copy link
Member

@Flash0ver Flash0ver Apr 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was wondering if we have already established prior art for the enum and it's name.

{
/// <summary>
/// Masks the view
/// </summary>
Mask,
/// <summary>
/// Unmasks the view
/// </summary>
Unmask
}

internal static class SessionReplayMaskModeExtensions
{
#if __ANDROID__
/// <summary>
/// Maps from <see cref="SessionReplayMaskMode"/> to the native tag values used by the JavaSDK to mask and unmask
/// views. See https://docs.sentry.io/platforms/android/session-replay/privacy/#mask-by-view-instance
/// </summary>
public static string ToNativeTag(this SessionReplayMaskMode maskMode) => maskMode switch
{
SessionReplayMaskMode.Mask => "sentry-mask",
SessionReplayMaskMode.Unmask => "sentry-unmask",
_ => throw new ArgumentOutOfRangeException(nameof(maskMode), maskMode, null)
};
#endif
}
12 changes: 12 additions & 0 deletions src/Sentry/Platforms/Android/NativeOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,18 @@ 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; } = [];

public void MaskControlsOfType<T>()
{
MaskedControls.Add(typeof(T));
}

public void UnmaskControlsOfType<T>()
{
UnmaskedControls.Add(typeof(T));
}
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
namespace Microsoft.Maui.Hosting
[assembly: Microsoft.Maui.Controls.XmlnsDefinition("http://schemas.sentry.io/maui", "Sentry.Maui")]
[assembly: Microsoft.Maui.Controls.XmlnsPrefix("http://schemas.sentry.io/maui", "sentry")]
namespace Microsoft.Maui.Hosting
{
public static class SentryMauiAppBuilderExtensions
{
Expand Down Expand Up @@ -36,6 +38,17 @@ namespace Sentry.Maui
public bool IncludeTitleInBreadcrumbs { get; set; }
public void SetBeforeScreenshotCapture(System.Func<Sentry.SentryEvent, Sentry.SentryHint, bool> beforeCapture) { }
}
public static class SessionReplay
{
public static readonly Microsoft.Maui.Controls.BindableProperty MaskProperty;
public static Sentry.Maui.SessionReplayMaskMode GetMask(Microsoft.Maui.Controls.BindableObject view) { }
public static void SetMask(Microsoft.Maui.Controls.BindableObject view, Sentry.Maui.SessionReplayMaskMode value) { }
}
public enum SessionReplayMaskMode
{
Mask = 0,
Unmask = 1,
}
}
namespace Sentry.Maui.Internal
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
namespace Microsoft.Maui.Hosting
[assembly: Microsoft.Maui.Controls.XmlnsDefinition("http://schemas.sentry.io/maui", "Sentry.Maui")]
[assembly: Microsoft.Maui.Controls.XmlnsPrefix("http://schemas.sentry.io/maui", "sentry")]
namespace Microsoft.Maui.Hosting
{
public static class SentryMauiAppBuilderExtensions
{
Expand Down Expand Up @@ -36,6 +38,17 @@ namespace Sentry.Maui
public bool IncludeTitleInBreadcrumbs { get; set; }
public void SetBeforeScreenshotCapture(System.Func<Sentry.SentryEvent, Sentry.SentryHint, bool> beforeCapture) { }
}
public static class SessionReplay
{
public static readonly Microsoft.Maui.Controls.BindableProperty MaskProperty;
public static Sentry.Maui.SessionReplayMaskMode GetMask(Microsoft.Maui.Controls.BindableObject view) { }
public static void SetMask(Microsoft.Maui.Controls.BindableObject view, Sentry.Maui.SessionReplayMaskMode value) { }
}
public enum SessionReplayMaskMode
{
Mask = 0,
Unmask = 1,
}
}
namespace Sentry.Maui.Internal
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
using Sentry.Maui.Internal;
using Sentry.Maui.Tests.Mocks;
#if __ANDROID__
using View = Android.Views.View;
#endif

namespace Sentry.Maui.Tests;

Expand Down
4 changes: 4 additions & 0 deletions test/Sentry.Maui.Tests/MauiEventsBinderTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ public Fixture()
hub.When(h => h.ConfigureScope(Arg.Any<Action<Scope>>()))
.Do(c => c.Arg<Action<Scope>>()(Scope));

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);
Binder = new MauiEventsBinder(
hub,
Expand Down
Loading
Loading