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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

### Features

- Expose screenshot masking options (`screenshot.maskAllText`, `screenshot.maskAllImages`, `screenshot.maskedViewClasses`, `screenshot.unmaskedViewClasses`) for error screenshots ([#6007](https://github.com/getsentry/sentry-react-native/pull/6007))
- Warn Expo users at Metro startup when prebuilt native projects are missing Sentry configuration ([#5984](https://github.com/getsentry/sentry-react-native/pull/5984))

### Dependencies
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,34 @@ final class RNSentryStartTests: XCTestCase {
}
}

func testScreenshotMaskingOptions() throws {
try startFromRN(options: [
"dsn": "https://abcd@efgh.ingest.sentry.io/123456",
"attachScreenshot": true,
"screenshot": [
"maskAllText": false,
"maskAllImages": true
]
])

let actualOptions = PrivateSentrySDKOnly.options
XCTAssertTrue(actualOptions.attachScreenshot)
XCTAssertFalse(actualOptions.screenshot.maskAllText)
XCTAssertTrue(actualOptions.screenshot.maskAllImages)
}

func testScreenshotMaskingOptionsDefaults() throws {
try startFromRN(options: [
"dsn": "https://abcd@efgh.ingest.sentry.io/123456",
"attachScreenshot": true
])

let actualOptions = PrivateSentrySDKOnly.options
XCTAssertTrue(actualOptions.attachScreenshot)
XCTAssertTrue(actualOptions.screenshot.maskAllText)
XCTAssertTrue(actualOptions.screenshot.maskAllImages)
}

func startFromRN(options: [String: Any]) throws {
var error: NSError?
RNSentryStart.start(options: options, error: &error)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import android.app.Activity;
import android.content.Context;
import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.bridge.ReadableType;
import com.facebook.react.common.JavascriptException;
Expand Down Expand Up @@ -136,6 +137,35 @@ static void getSentryAndroidOptions(
if (rnOptions.hasKey("attachScreenshot")) {
options.setAttachScreenshot(rnOptions.getBoolean("attachScreenshot"));
}
if (rnOptions.hasKey("screenshot")) {
@Nullable final ReadableMap screenshotOptions = rnOptions.getMap("screenshot");
if (screenshotOptions != null) {
if (screenshotOptions.hasKey("maskAllText")) {
options.getScreenshot().setMaskAllText(screenshotOptions.getBoolean("maskAllText"));
}
if (screenshotOptions.hasKey("maskAllImages")) {
options.getScreenshot().setMaskAllImages(screenshotOptions.getBoolean("maskAllImages"));
}
if (screenshotOptions.hasKey("maskedViewClasses")) {
@Nullable
final ReadableArray maskedClasses = screenshotOptions.getArray("maskedViewClasses");
if (maskedClasses != null) {
for (int i = 0; i < maskedClasses.size(); i++) {
options.getScreenshot().addMaskViewClass(maskedClasses.getString(i));
}
}
}
if (screenshotOptions.hasKey("unmaskedViewClasses")) {
@Nullable
final ReadableArray unmaskedClasses = screenshotOptions.getArray("unmaskedViewClasses");
if (unmaskedClasses != null) {
for (int i = 0; i < unmaskedClasses.size(); i++) {
options.getScreenshot().addUnmaskViewClass(unmaskedClasses.getString(i));
}
}
}
}
}
if (rnOptions.hasKey("attachViewHierarchy")) {
options.setAttachViewHierarchy(rnOptions.getBoolean("attachViewHierarchy"));
}
Expand Down
40 changes: 40 additions & 0 deletions packages/core/ios/RNSentryStart.m
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,46 @@ + (SentryOptions *_Nullable)createOptionsWithDictionary:(NSDictionary *_Nonnull)
return nil;
}

#if SENTRY_HAS_UIKIT
NSDictionary *screenshotDict = mutableOptions[@"screenshot"];
if ([screenshotDict isKindOfClass:[NSDictionary class]]) {
id maskAllText = screenshotDict[@"maskAllText"];
if ([maskAllText isKindOfClass:[NSNumber class]]) {
sentryOptions.screenshot.maskAllText = [maskAllText boolValue];
}
id maskAllImages = screenshotDict[@"maskAllImages"];
if ([maskAllImages isKindOfClass:[NSNumber class]]) {
sentryOptions.screenshot.maskAllImages = [maskAllImages boolValue];
}
id maskedViewClasses = screenshotDict[@"maskedViewClasses"];
if ([maskedViewClasses isKindOfClass:[NSArray class]]) {
NSMutableArray *classes = [NSMutableArray array];
for (id className in maskedViewClasses) {
if ([className isKindOfClass:[NSString class]]) {
Class cls = NSClassFromString(className);
if (cls != nil) {
[classes addObject:cls];
}
}
}
sentryOptions.screenshot.maskedViewClasses = classes;
}
id unmaskedViewClasses = screenshotDict[@"unmaskedViewClasses"];
if ([unmaskedViewClasses isKindOfClass:[NSArray class]]) {
NSMutableArray *classes = [NSMutableArray array];
for (id className in unmaskedViewClasses) {
if ([className isKindOfClass:[NSString class]]) {
Class cls = NSClassFromString(className);
if (cls != nil) {
[classes addObject:cls];
}
}
}
sentryOptions.screenshot.unmaskedViewClasses = classes;
}
}
#endif

// Exclude Dev Server and Sentry Dsn request from Breadcrumbs
NSString *dsn = [self getURLFromDSN:[mutableOptions valueForKey:@"dsn"]];
// TODO: For Auto Init from JS dev server is resolved automatically, for init from options file
Expand Down
31 changes: 31 additions & 0 deletions packages/core/src/js/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,37 @@ export interface BaseReactNativeOptions {
*/
attachScreenshot?: boolean;

/**
* Options for configuring screenshot masking on error screenshots.
* When `attachScreenshot` is enabled, these options control what gets masked in the screenshot.
*/
screenshot?: {
/**
* Mask all text content in error screenshots.
*
* @default true
*/
maskAllText?: boolean;
/**
* Mask all images in error screenshots.
*
* @default true
*/
maskAllImages?: boolean;
/**
* A list of native view class names to mask in error screenshots.
* Useful for masking views from third-party native libraries (e.g., map views, payment forms).
*
* @example ['com.example.MyCustomView'] // Android
* @example ['MKMapView'] // iOS
*/
maskedViewClasses?: string[];
/**
* A list of native view class names to exclude from masking in error screenshots.
*/
unmaskedViewClasses?: string[];
};

/**
* When enabled Sentry includes the current view hierarchy in the error attachments.
*
Expand Down
26 changes: 26 additions & 0 deletions packages/core/test/wrapper.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -414,6 +414,32 @@ describe('Tests Native Wrapper', () => {
expect(initParameter.strictTraceContinuation).toBe(true);
});

test('passes screenshot options to native SDK', async () => {
await NATIVE.initNativeSdk({
dsn: 'test',
enableNative: true,
autoInitializeNativeSdk: true,
screenshot: {
maskAllText: false,
maskAllImages: true,
maskedViewClasses: ['com.example.MyView'],
unmaskedViewClasses: ['com.example.SafeView'],
},
devServerUrl: undefined,
defaultSidecarUrl: undefined,
mobileReplayOptions: undefined,
});

expect(RNSentry.initNativeSdk).toHaveBeenCalled();
const initParameter = (RNSentry.initNativeSdk as jest.MockedFunction<any>).mock.calls[0][0];
expect(initParameter.screenshot).toEqual({
maskAllText: false,
maskAllImages: true,
maskedViewClasses: ['com.example.MyView'],
unmaskedViewClasses: ['com.example.SafeView'],
});
});

test('passes orgId option to native SDK', async () => {
await NATIVE.initNativeSdk({
dsn: 'test',
Expand Down
5 changes: 5 additions & 0 deletions samples/react-native/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,11 @@ Sentry.init({
attachStacktrace: true,
// Attach screenshots to events.
attachScreenshot: true,
// Screenshot masking options.
screenshot: {
maskAllText: true,
maskAllImages: true,
},
// Attach view hierarchy to events.
attachViewHierarchy: true,
// Enables capture failed requests in JS and native.
Expand Down
Loading