From 4984ce381e19737f5895282628f072c1c59a5dbf Mon Sep 17 00:00:00 2001 From: Roman Zavarnitsyn Date: Thu, 23 Oct 2025 13:43:08 +0200 Subject: [PATCH 1/5] feat(replay): Add screenshotStrategy option for Android --- .../io/sentry/react/RNSentryModuleImpl.java | 25 +++++++++++++++++++ packages/core/src/js/integrations/exports.ts | 2 +- packages/core/src/js/replay/mobilereplay.ts | 22 ++++++++++++++++ samples/react-native/src/App.tsx | 1 + 4 files changed, 49 insertions(+), 1 deletion(-) diff --git a/packages/core/android/src/main/java/io/sentry/react/RNSentryModuleImpl.java b/packages/core/android/src/main/java/io/sentry/react/RNSentryModuleImpl.java index 3b9bbf30f1..f375fa0a17 100644 --- a/packages/core/android/src/main/java/io/sentry/react/RNSentryModuleImpl.java +++ b/packages/core/android/src/main/java/io/sentry/react/RNSentryModuleImpl.java @@ -37,6 +37,7 @@ import io.sentry.ISerializer; import io.sentry.Integration; import io.sentry.ScopesAdapter; +import io.sentry.ScreenshotStrategyType; import io.sentry.Sentry; import io.sentry.SentryDate; import io.sentry.SentryDateProvider; @@ -415,12 +416,36 @@ private SentryReplayOptions getReplayOptions(@NotNull ReadableMap rnOptions) { androidReplayOptions.addMaskViewClass("com.horcrux.svg.SvgView"); // react-native-svg } + if (rnMobileReplayOptions.hasKey("screenshotStrategy")) { + final String screenshotStrategyString = rnMobileReplayOptions.getString("screenshotStrategy"); + final ScreenshotStrategyType screenshotStrategy = + parseScreenshotStrategy(screenshotStrategyString); + androidReplayOptions.setScreenshotStrategy(screenshotStrategy); + } + androidReplayOptions.setMaskViewContainerClass(RNSentryReplayMask.class.getName()); androidReplayOptions.setUnmaskViewContainerClass(RNSentryReplayUnmask.class.getName()); return androidReplayOptions; } + private ScreenshotStrategyType parseScreenshotStrategy(@Nullable String strategyString) { + if (strategyString == null) { + return ScreenshotStrategyType.PIXEL_COPY; + } + + try { + switch (strategyString.toLowerCase(Locale.ROOT)) { + case "canvas": + return ScreenshotStrategyType.CANVAS; + default: + return ScreenshotStrategyType.PIXEL_COPY; + } + } catch (Exception e) { + return ScreenshotStrategyType.PIXEL_COPY; + } + } + private SentryReplayQuality parseReplayQuality(@Nullable String qualityString) { if (qualityString == null) { return SentryReplayQuality.MEDIUM; diff --git a/packages/core/src/js/integrations/exports.ts b/packages/core/src/js/integrations/exports.ts index 4f4d0fb0ac..484c6a72cb 100644 --- a/packages/core/src/js/integrations/exports.ts +++ b/packages/core/src/js/integrations/exports.ts @@ -12,7 +12,7 @@ export { screenshotIntegration } from './screenshot'; export { viewHierarchyIntegration } from './viewhierarchy'; export { expoContextIntegration } from './expocontext'; export { spotlightIntegration } from './spotlight'; -export { mobileReplayIntegration } from '../replay/mobilereplay'; +export { mobileReplayIntegration, ScreenshotStrategy } from '../replay/mobilereplay'; export { feedbackIntegration } from '../feedback/integration'; export { browserReplayIntegration } from '../replay/browserReplay'; export { appStartIntegration } from '../tracing/integrations/appStart'; diff --git a/packages/core/src/js/replay/mobilereplay.ts b/packages/core/src/js/replay/mobilereplay.ts index 34b32a811e..af84d0a01a 100644 --- a/packages/core/src/js/replay/mobilereplay.ts +++ b/packages/core/src/js/replay/mobilereplay.ts @@ -8,6 +8,14 @@ import { enrichXhrBreadcrumbsForMobileReplay } from './xhrUtils'; export const MOBILE_REPLAY_INTEGRATION_NAME = 'MobileReplay'; +/** + * Screenshot strategy type for Android Session Replay. + * + * - `'canvas'`: Canvas-based screenshot strategy. This strategy does **not** support any masking options, it always masks text and images. Use this if your application has strict PII requirements. + * - `'pixelCopy'`: Pixel copy screenshot strategy (default). Supports all masking options. + */ +export type ScreenshotStrategy = 'canvas' | 'pixelCopy'; + export interface MobileReplayOptions { /** * Mask all text in recordings @@ -69,6 +77,19 @@ export interface MobileReplayOptions { * @default false */ enableFastViewRendering?: boolean; + + /** + * Sets the screenshot strategy used by the Session Replay integration on Android. + * + * If your application has strict PII requirements we recommend using `'canvas'`. + * This strategy does **not** support any masking options, it always masks text and images. + * + * - Experiment: In case you are noticing issues with the canvas screenshot strategy, please report the issue on [GitHub](https://github.com/getsentry/sentry-java). + * + * @default 'pixelCopy' + * @platform android + */ + screenshotStrategy?: ScreenshotStrategy; } const defaultOptions: Required = { @@ -78,6 +99,7 @@ const defaultOptions: Required = { enableExperimentalViewRenderer: false, enableViewRendererV2: true, enableFastViewRendering: false, + screenshotStrategy: 'pixelCopy', }; function mergeOptions(initOptions: Partial): Required { diff --git a/samples/react-native/src/App.tsx b/samples/react-native/src/App.tsx index fca0fe3f64..c9a24aea6f 100644 --- a/samples/react-native/src/App.tsx +++ b/samples/react-native/src/App.tsx @@ -94,6 +94,7 @@ Sentry.init({ maskAllVectors: true, maskAllText: true, enableViewRendererV2: true, + screenshotStrategy: 'canvas', // if you have strict PII requirements }), Sentry.appStartIntegration({ standalone: false, From 851c466230d5fd05a52a28ab17e7d18cf94def91 Mon Sep 17 00:00:00 2001 From: Roman Zavarnitsyn Date: Thu, 23 Oct 2025 13:51:19 +0200 Subject: [PATCH 2/5] Remove type export --- packages/core/src/js/integrations/exports.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/src/js/integrations/exports.ts b/packages/core/src/js/integrations/exports.ts index 484c6a72cb..4f4d0fb0ac 100644 --- a/packages/core/src/js/integrations/exports.ts +++ b/packages/core/src/js/integrations/exports.ts @@ -12,7 +12,7 @@ export { screenshotIntegration } from './screenshot'; export { viewHierarchyIntegration } from './viewhierarchy'; export { expoContextIntegration } from './expocontext'; export { spotlightIntegration } from './spotlight'; -export { mobileReplayIntegration, ScreenshotStrategy } from '../replay/mobilereplay'; +export { mobileReplayIntegration } from '../replay/mobilereplay'; export { feedbackIntegration } from '../feedback/integration'; export { browserReplayIntegration } from '../replay/browserReplay'; export { appStartIntegration } from '../tracing/integrations/appStart'; From 54ba0e8c89e190ff58d39ee77dfc3a44dd21ca53 Mon Sep 17 00:00:00 2001 From: Roman Zavarnitsyn Date: Thu, 23 Oct 2025 13:57:11 +0200 Subject: [PATCH 3/5] Changelog --- CHANGELOG.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ab3f57bfa1..1d78a5f340 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,26 @@ ## Unreleased +### Features + +- Add new _experimental_ Canvas Capture Strategy for Session Replay ([#5301](https://github.com/getsentry/sentry-react-native/pull/5301)) + - A new screenshot capture strategy that uses Android's Canvas API for more accurate and reliable text and image masking + - Any .drawText() or .drawBitmap() calls are replaced by rectangles, ensuring no text or images are present in the resulting output + - Note: If this strategy is used, all text and images will be masked, regardless of any masking configuration + - To enable this feature, set the `screenshotStrategy` to `canvas`: + ```js + import * as Sentry from '@sentry/react-native'; + + Sentry.init({ + integrations: [ + Sentry.mobileReplayIntegration({ + screenshotStrategy: 'canvas', + }), + ], + }); + ``` + + ### Dependencies - Bump Bundler Plugins from v4.4.0 to v4.5.0 ([#5283](https://github.com/getsentry/sentry-react-native/pull/5283)) From a648626583b215c7132c797099ac8a0514065a3b Mon Sep 17 00:00:00 2001 From: Roman Zavarnitsyn Date: Thu, 23 Oct 2025 13:59:39 +0200 Subject: [PATCH 4/5] Changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index def33f8c28..9eb3ad7f4a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,7 +13,7 @@ - Adds GraphQL integration ([#5299](https://github.com/getsentry/sentry-react-native/pull/5299)) - Add new _experimental_ Canvas Capture Strategy for Session Replay ([#5301](https://github.com/getsentry/sentry-react-native/pull/5301)) - A new screenshot capture strategy that uses Android's Canvas API for more accurate and reliable text and image masking - - Any .drawText() or .drawBitmap() calls are replaced by rectangles, ensuring no text or images are present in the resulting output + - Any `.drawText()` or `.drawBitmap()` calls are replaced by rectangles, ensuring no text or images are present in the resulting output - Note: If this strategy is used, all text and images will be masked, regardless of any masking configuration - To enable this feature, set the `screenshotStrategy` to `canvas`: ```js From aef23db7861d8089374afa6f1664b3d3786be7b0 Mon Sep 17 00:00:00 2001 From: Roman Zavarnitsyn Date: Wed, 29 Oct 2025 09:37:25 +0100 Subject: [PATCH 5/5] Update packages/core/android/src/main/java/io/sentry/react/RNSentryModuleImpl.java Co-authored-by: Antonis Lilis --- .../java/io/sentry/react/RNSentryModuleImpl.java | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/packages/core/android/src/main/java/io/sentry/react/RNSentryModuleImpl.java b/packages/core/android/src/main/java/io/sentry/react/RNSentryModuleImpl.java index f375fa0a17..c5421c0d5b 100644 --- a/packages/core/android/src/main/java/io/sentry/react/RNSentryModuleImpl.java +++ b/packages/core/android/src/main/java/io/sentry/react/RNSentryModuleImpl.java @@ -434,15 +434,11 @@ private ScreenshotStrategyType parseScreenshotStrategy(@Nullable String strategy return ScreenshotStrategyType.PIXEL_COPY; } - try { - switch (strategyString.toLowerCase(Locale.ROOT)) { - case "canvas": - return ScreenshotStrategyType.CANVAS; - default: - return ScreenshotStrategyType.PIXEL_COPY; - } - } catch (Exception e) { - return ScreenshotStrategyType.PIXEL_COPY; + switch (strategyString.toLowerCase(Locale.ROOT)) { + case "canvas": + return ScreenshotStrategyType.CANVAS; + default: + return ScreenshotStrategyType.PIXEL_COPY; } }