Skip to content
Open
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
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,22 @@
### Features

- 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
- 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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 {
Copy link
Collaborator

Choose a reason for hiding this comment

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

q: why the try/catch block here?

Copy link
Member Author

Choose a reason for hiding this comment

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

I just followed the pattern for quality, but can remove it

Copy link
Contributor

@antonis antonis Oct 24, 2025

Choose a reason for hiding this comment

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

I don't recall why I added the try/catch block for quality 😓
I probably followed an existing pattern or modified the code from a state that it was needed. I think it is not needed and opened #5305 to fix that.

switch (strategyString.toLowerCase(Locale.ROOT)) {
case "canvas":
return ScreenshotStrategyType.CANVAS;
default:
return ScreenshotStrategyType.PIXEL_COPY;
}
} catch (Exception e) {
return ScreenshotStrategyType.PIXEL_COPY;
}
Comment on lines +437 to +446
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: Following up on https://github.com/getsentry/sentry-react-native/pull/5301/files#r2461084612

Suggested change
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;
}

}

private SentryReplayQuality parseReplayQuality(@Nullable String qualityString) {
if (qualityString == null) {
return SentryReplayQuality.MEDIUM;
Expand Down
22 changes: 22 additions & 0 deletions packages/core/src/js/replay/mobilereplay.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Copy link
Collaborator

Choose a reason for hiding this comment

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

@alwx @antonis What do you think of updating other platform specific options with @platform?

Copy link
Contributor

Choose a reason for hiding this comment

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

I think that's a good idea 👍

*/
screenshotStrategy?: ScreenshotStrategy;
}

const defaultOptions: Required<MobileReplayOptions> = {
Expand All @@ -78,6 +99,7 @@ const defaultOptions: Required<MobileReplayOptions> = {
enableExperimentalViewRenderer: false,
enableViewRendererV2: true,
enableFastViewRendering: false,
screenshotStrategy: 'pixelCopy',
Copy link
Collaborator

Choose a reason for hiding this comment

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

might be worth on a major change to default it to canvas if sendDefaultPii is not true.

Copy link
Member Author

Choose a reason for hiding this comment

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

yeah we'll monitor the adoption and possibly make it a default in the future

};

function mergeOptions(initOptions: Partial<MobileReplayOptions>): Required<MobileReplayOptions> {
Expand Down
1 change: 1 addition & 0 deletions samples/react-native/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ Sentry.init({
maskAllVectors: true,
maskAllText: true,
enableViewRendererV2: true,
screenshotStrategy: 'canvas', // if you have strict PII requirements
}),
Sentry.appStartIntegration({
standalone: false,
Expand Down
Loading