- 
          
- 
                Notifications
    You must be signed in to change notification settings 
- Fork 225
Custom SessionReplay masks in MAUI Android apps #4121
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
e8a65cf
              4acad3a
              9d26a64
              efb8452
              ba18078
              3460c28
              d518620
              928acca
              8919d10
              0e7cfc1
              47d8b75
              b362f07
              a387112
              File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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")] | 
| 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 | ||
| } | ||
| } | 
| 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 | ||
| } | ||
| 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 | ||
| There was a problem hiding this comment. Choose a reason for hiding this commentThe 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  There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.