Skip to content

Commit c61a7be

Browse files
feat: XML Views Automasking options (#299)
## Summary Support maskText, maskTextInput, maskSensitive for Android Native XML Views ## How did you test this change? <!-- Frontend - Leave a screencast or a screenshot to visually describe the changes. --> ## Are there any deployment considerations? <!-- Backend - Do we need to consider migrations or backfilling data? --> <!-- CURSOR_SUMMARY --> --- > [!NOTE] > Adds auto-masking for Android XML views and refactors masking to a new MaskTarget/MaskMatcher API, updating capture/instrumentation and e2e demo package paths. > > - **SDK • Session Replay/Masking**: > - **New masking module** `replay/masking`: introduce `MaskTarget`/`MaskMatcher`, `ComposeMaskTarget`, `NativeMaskTarget` (handles `EditText`/`TextView`, passwords, hints, contentDescription), and `SensitiveAreasCollector` (now uses `LDLogger`). > - **Privacy**: `PrivacyProfile` matchers rewritten to operate on `MaskTarget` (`textInput`, `text`, `sensitive`). > - **Capture/Instrumentation**: `CaptureSource` now accepts `LDLogger` and uses new `masking.SensitiveAreasCollector`; `ReplayInstrumentation` creates logger and passes it to `CaptureSource`. > - Remove old `replay/MaskMatcher.kt` and old `SensitiveAreasCollector`. > - **E2E App**: > - Move Smoothie demo to `com.example.androidobservability.smoothie`; update `AndroidManifest.xml`, `MainActivity`, and adapter imports. > - `SmoothieAdapter`: call `ldMask()` on image view to mark as sensitive. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 60a4713. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
1 parent 6e6c388 commit c61a7be

File tree

14 files changed

+364
-262
lines changed

14 files changed

+364
-262
lines changed

e2e/android/app/src/main/AndroidManifest.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
android:exported="false"
2727
android:theme="@style/Theme.AndroidObservability" />
2828
<activity
29-
android:name="com.smoothie.SmoothieListActivity"
29+
android:name=".smoothie.SmoothieListActivity"
3030
android:exported="false"
3131
android:theme="@style/Theme.AndroidObservability" />
3232
<service

e2e/android/app/src/main/java/com/example/androidobservability/MainActivity.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import androidx.compose.runtime.remember
2424
import androidx.compose.runtime.setValue
2525
import androidx.compose.ui.Modifier
2626
import androidx.compose.ui.unit.dp
27+
import com.example.androidobservability.smoothie.SmoothieListActivity
2728
import com.example.androidobservability.ui.theme.AndroidObservabilityTheme
2829

2930
class MainActivity : ComponentActivity() {
@@ -86,7 +87,7 @@ class MainActivity : ComponentActivity() {
8687
this@MainActivity.startActivity(
8788
Intent(
8889
this@MainActivity,
89-
com.smoothie.SmoothieListActivity::class.java
90+
SmoothieListActivity::class.java
9091
)
9192
)
9293
}

e2e/android/app/src/main/java/com/smoothie/SmoothieAdapter.kt renamed to e2e/android/app/src/main/java/com/example/androidobservability/smoothie/SmoothieAdapter.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package com.smoothie
1+
package com.example.androidobservability.smoothie
22

33
import android.graphics.Bitmap
44
import android.view.LayoutInflater

e2e/android/app/src/main/java/com/smoothie/SmoothieListActivity.kt renamed to e2e/android/app/src/main/java/com/example/androidobservability/smoothie/SmoothieListActivity.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package com.smoothie
1+
package com.example.androidobservability.smoothie
22

33
import android.graphics.BitmapFactory
44
import android.os.Bundle

sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/replay/CaptureSource.kt

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,10 @@ import android.os.Looper
1414
import android.util.Base64
1515
import android.view.Choreographer
1616
import android.view.PixelCopy
17+
import com.launchdarkly.logging.LDLogger
1718
import com.launchdarkly.observability.coroutines.DispatcherProviderHolder
19+
import com.launchdarkly.observability.replay.masking.MaskMatcher
20+
import com.launchdarkly.observability.replay.masking.SensitiveAreasCollector
1821
import io.opentelemetry.android.session.SessionManager
1922
import kotlinx.coroutines.CoroutineScope
2023
import kotlinx.coroutines.flow.MutableSharedFlow
@@ -37,6 +40,7 @@ import androidx.compose.ui.geometry.Rect as ComposeRect
3740
class CaptureSource(
3841
private val sessionManager: SessionManager,
3942
private val maskMatchers: List<MaskMatcher>,
43+
private val logger: LDLogger,
4044
// TODO: O11Y-628 - add captureQuality options
4145
) :
4246
Application.ActivityLifecycleCallbacks {
@@ -46,7 +50,7 @@ class CaptureSource(
4650
private val _captureEventFlow = MutableSharedFlow<CaptureEvent>()
4751
val captureFlow: SharedFlow<CaptureEvent> = _captureEventFlow.asSharedFlow()
4852

49-
private val sensitiveAreasCollector = SensitiveAreasCollector()
53+
private val sensitiveAreasCollector = SensitiveAreasCollector(logger)
5054

5155
/**
5256
* Attaches the [CaptureSource] to the [Application] whose [Activity]s will be captured.

sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/replay/MaskMatcher.kt

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

sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/replay/PrivacyProfile.kt

Lines changed: 9 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
11
package com.launchdarkly.observability.replay
22

3-
import androidx.compose.ui.semantics.SemanticsActions
4-
import androidx.compose.ui.semantics.SemanticsNode
5-
import androidx.compose.ui.semantics.SemanticsProperties
6-
import androidx.compose.ui.semantics.getOrNull
3+
import com.launchdarkly.observability.replay.masking.MaskMatcher
4+
import com.launchdarkly.observability.replay.masking.MaskTarget
75

86
/**
97
* [PrivacyProfile] encapsulates options and functionality related to privacy of session
@@ -15,7 +13,7 @@ import androidx.compose.ui.semantics.getOrNull
1513
* @param maskTextInputs set to false to turn off masking text inputs
1614
* @param maskText set to false to turn off masking text
1715
* @param maskSensitive set to false to turn off masking sensitive views
18-
* @param maskAdditionalMatchers list of additional [MaskMatcher]s that will be masked when they match
16+
* @param maskAdditionalMatchers list of additional [com.launchdarkly.observability.replay.masking.MaskMatcher]s that will be masked when they match
1917
**/
2018
data class PrivacyProfile(
2119
val maskTextInputs: Boolean = true,
@@ -40,12 +38,8 @@ data class PrivacyProfile(
4038
* miss as we can't account for all possible future semantic properties.
4139
*/
4240
val textInputMatcher: MaskMatcher = object : MaskMatcher {
43-
override fun isMatch(node: SemanticsNode): Boolean {
44-
val config = node.config
45-
return config.contains(SemanticsProperties.EditableText) ||
46-
config.contains(SemanticsActions.SetText) ||
47-
config.contains(SemanticsActions.PasteText) ||
48-
config.contains(SemanticsActions.InsertTextAtCursor)
41+
override fun isMatch(target: MaskTarget): Boolean {
42+
return target.isTextInput()
4943
}
5044
}
5145

@@ -54,8 +48,8 @@ data class PrivacyProfile(
5448
* miss as we can't account for all possible future semantic properties.
5549
*/
5650
val textMatcher: MaskMatcher = object : MaskMatcher {
57-
override fun isMatch(node: SemanticsNode): Boolean {
58-
return node.config.contains(SemanticsProperties.Text)
51+
override fun isMatch(target: MaskTarget): Boolean {
52+
return target.isText()
5953
}
6054
}
6155

@@ -64,39 +58,8 @@ data class PrivacyProfile(
6458
* and all text or context descriptions that have substring matches with any of the [sensitiveKeywords]
6559
*/
6660
val sensitiveMatcher: MaskMatcher = object : MaskMatcher {
67-
override fun isMatch(node: SemanticsNode): Boolean {/**/
68-
if (node.config.contains(SemanticsProperties.Password)) {
69-
return true
70-
}
71-
72-
// check text first for performance, more likely to get a match here than in description below
73-
val textValues = node.config.getOrNull(SemanticsProperties.Text)
74-
if (textValues != null) {
75-
if (textValues.any { annotated ->
76-
val lowerText = annotated.text.lowercase()
77-
sensitiveKeywords.any { keyword ->
78-
// could use ignoreCase = true here, but that is less
79-
// performant than lower casing desc once above
80-
lowerText.contains(keyword)
81-
}
82-
}) return true
83-
}
84-
85-
// check content description
86-
val contentDescriptions =
87-
node.config.getOrNull(SemanticsProperties.ContentDescription)
88-
if (contentDescriptions != null) {
89-
if (contentDescriptions.any { desc ->
90-
val lowerDesc = desc.lowercase()
91-
sensitiveKeywords.any { keyword ->
92-
// could use ignoreCase = true here, but that is less
93-
// performant than lower casing desc once above
94-
lowerDesc.contains(keyword)
95-
}
96-
}) return true
97-
}
98-
99-
return false
61+
override fun isMatch(target: MaskTarget): Boolean {
62+
return target.isSensitive(sensitiveKeywords)
10063
}
10164
}
10265

sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/replay/ReplayInstrumentation.kt

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.launchdarkly.observability.replay
22

3+
import com.launchdarkly.logging.LDLogger
34
import com.launchdarkly.observability.coroutines.DispatcherProviderHolder
45
import com.launchdarkly.observability.interfaces.LDExtendedInstrumentation
56
import io.opentelemetry.android.instrumentation.InstallationContext
@@ -68,7 +69,6 @@ class ReplayInstrumentation(
6869
private lateinit var _otelLogger: Logger
6970
private lateinit var _captureSource: CaptureSource
7071
private lateinit var _interactionSource: InteractionSource
71-
7272
private var _captureJob: Job? = null
7373
private var _isPaused: Boolean = false
7474
private val _captureMutex = Mutex()
@@ -77,7 +77,9 @@ class ReplayInstrumentation(
7777

7878
override fun install(ctx: InstallationContext) {
7979
_otelLogger = ctx.openTelemetry.logsBridge.get(INSTRUMENTATION_SCOPE_NAME)
80-
_captureSource = CaptureSource(ctx.sessionManager, options.privacyProfile.asMatchersList())
80+
// TODO: Use real LDClient logger after creating SR Plugin
81+
val logger = LDLogger.none()
82+
_captureSource = CaptureSource(ctx.sessionManager, options.privacyProfile.asMatchersList(), logger)
8183
_interactionSource = InteractionSource(ctx.sessionManager)
8284

8385
// TODO: O11Y-621 - don't use global scope

sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/replay/SensitiveAreasCollector.kt

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

0 commit comments

Comments
 (0)