diff --git a/CHANGELOG.md b/CHANGELOG.md index b6dd3546c9..99aabd1e1c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,15 @@ ### Fixes - `browserReplayIntegration` is no longer included by default on React Native Web ([#4270](https://github.com/getsentry/sentry-react-native/pull/4270), [#4308](https://github.com/getsentry/sentry-react-native/pull/4308)) +- Replay `maskAll*` set to `false` on iOS kept all masked ([#4257](https://github.com/getsentry/sentry-react-native/pull/4257), [#4309](https://github.com/getsentry/sentry-react-native/pull/4309)) +- `browserReplayIntegration` is no longer included by default on React Native Web ([#4270](https://github.com/getsentry/sentry-react-native/pull/4270), [#4308](https://github.com/getsentry/sentry-react-native/pull/4308)) +- Fix Replay redacting of RN Classes on iOS ([#4243](https://github.com/getsentry/sentry-react-native/pull/4243), [#4309](https://github.com/getsentry/sentry-react-native/pull/4309)) + +### Dependencies + +- Bump Cocoa SDK from v8.37.0 to v8.41.0 ([#4309](https://github.com/getsentry/sentry-react-native/pull/4309)) + - [changelog](https://github.com/getsentry/sentry-cocoa/blob/main/CHANGELOG.md#8410) + - [diff](https://github.com/getsentry/sentry-cocoa/compare/8.37.0...8.41.0) ## 5.35.0 diff --git a/RNSentry.podspec b/RNSentry.podspec index 1c902a7c2f..07c81023a3 100644 --- a/RNSentry.podspec +++ b/RNSentry.podspec @@ -37,7 +37,7 @@ Pod::Spec.new do |s| s.compiler_flags = other_cflags - s.dependency 'Sentry/HybridSDK', '8.37.0' + s.dependency 'Sentry/HybridSDK', '8.41.0' if defined? install_modules_dependencies # Default React Native dependencies for 0.71 and above (new and legacy architecture) diff --git a/RNSentryCocoaTester/RNSentryCocoaTester.xcodeproj/project.pbxproj b/RNSentryCocoaTester/RNSentryCocoaTester.xcodeproj/project.pbxproj index 8394208456..653cf00eff 100644 --- a/RNSentryCocoaTester/RNSentryCocoaTester.xcodeproj/project.pbxproj +++ b/RNSentryCocoaTester/RNSentryCocoaTester.xcodeproj/project.pbxproj @@ -7,11 +7,11 @@ objects = { /* Begin PBXBuildFile section */ - 330F308C2C0F3840002A0D4E /* RNSentryBreadcrumbTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 330F308B2C0F3840002A0D4E /* RNSentryBreadcrumbTests.m */; }; 336084392C32E382008CC412 /* RNSentryReplayBreadcrumbConverterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 336084382C32E382008CC412 /* RNSentryReplayBreadcrumbConverterTests.swift */; }; 33958C692BFCF12600AD1FB6 /* RNSentryOnDrawReporterTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 33958C682BFCF12600AD1FB6 /* RNSentryOnDrawReporterTests.m */; }; 33AFDFED2B8D14B300AAB120 /* RNSentryFramesTrackerListenerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 33AFDFEC2B8D14B300AAB120 /* RNSentryFramesTrackerListenerTests.m */; }; 33AFDFF12B8D15E500AAB120 /* RNSentryDependencyContainerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 33AFDFF02B8D15E500AAB120 /* RNSentryDependencyContainerTests.m */; }; + 33BB05DF2CF4C72400EE1DD1 /* RNSentryReplayOptionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33BB05DE2CF4C72400EE1DD1 /* RNSentryReplayOptionsTests.swift */; }; 33F58AD02977037D008F60EA /* RNSentryTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 33F58ACF2977037D008F60EA /* RNSentryTests.mm */; }; B5859A50A3E865EF5E61465A /* libPods-RNSentryCocoaTesterTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 650CB718ACFBD05609BF2126 /* libPods-RNSentryCocoaTesterTests.a */; }; /* End PBXBuildFile section */ @@ -19,7 +19,6 @@ /* Begin PBXFileReference section */ 1482D5685A340AB93348A43D /* Pods-RNSentryCocoaTesterTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RNSentryCocoaTesterTests.release.xcconfig"; path = "Target Support Files/Pods-RNSentryCocoaTesterTests/Pods-RNSentryCocoaTesterTests.release.xcconfig"; sourceTree = ""; }; 330F308D2C0F385A002A0D4E /* RNSentryBreadcrumb.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = RNSentryBreadcrumb.h; path = ../ios/RNSentryBreadcrumb.h; sourceTree = ""; }; - 336084372C32E382008CC412 /* RNSentryCocoaTesterTests-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "RNSentryCocoaTesterTests-Bridging-Header.h"; sourceTree = ""; }; 336084382C32E382008CC412 /* RNSentryReplayBreadcrumbConverterTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RNSentryReplayBreadcrumbConverterTests.swift; sourceTree = ""; }; 3360843A2C32E3A8008CC412 /* RNSentryReplayBreadcrumbConverter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = RNSentryReplayBreadcrumbConverter.h; path = ../ios/RNSentryReplayBreadcrumbConverter.h; sourceTree = ""; }; 3360843C2C340C76008CC412 /* RNSentryBreadcrumbTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RNSentryBreadcrumbTests.swift; sourceTree = ""; }; @@ -32,6 +31,8 @@ 33AFDFF02B8D15E500AAB120 /* RNSentryDependencyContainerTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RNSentryDependencyContainerTests.m; sourceTree = ""; }; 33AFDFF22B8D15F600AAB120 /* RNSentryDependencyContainerTests.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RNSentryDependencyContainerTests.h; sourceTree = ""; }; 33AFE0132B8F31AF00AAB120 /* RNSentryDependencyContainer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = RNSentryDependencyContainer.h; path = ../ios/RNSentryDependencyContainer.h; sourceTree = ""; }; + 33BB05DE2CF4C72400EE1DD1 /* RNSentryReplayOptionsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RNSentryReplayOptionsTests.swift; sourceTree = ""; }; + 33BB05E02CF4C77C00EE1DD1 /* RNSentryReplay.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = RNSentryReplay.h; path = ../ios/RNSentryReplay.h; sourceTree = SOURCE_ROOT; }; 33F58ACF2977037D008F60EA /* RNSentryTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = RNSentryTests.mm; sourceTree = ""; }; 650CB718ACFBD05609BF2126 /* libPods-RNSentryCocoaTesterTests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-RNSentryCocoaTesterTests.a"; sourceTree = BUILT_PRODUCTS_DIR; }; E2321E7CFA55AB617247098E /* Pods-RNSentryCocoaTesterTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RNSentryCocoaTesterTests.debug.xcconfig"; path = "Target Support Files/Pods-RNSentryCocoaTesterTests/Pods-RNSentryCocoaTesterTests.debug.xcconfig"; sourceTree = ""; }; @@ -80,6 +81,7 @@ 3360899029524164007C7730 /* RNSentryCocoaTesterTests */ = { isa = PBXGroup; children = ( + 33BB05DE2CF4C72400EE1DD1 /* RNSentryReplayOptionsTests.swift */, 336084382C32E382008CC412 /* RNSentryReplayBreadcrumbConverterTests.swift */, 33F58ACF2977037D008F60EA /* RNSentryTests.mm */, 338739072A7D7D2800950DDD /* RNSentryTests.h */, @@ -89,7 +91,6 @@ 33AFDFF22B8D15F600AAB120 /* RNSentryDependencyContainerTests.h */, 33958C682BFCF12600AD1FB6 /* RNSentryOnDrawReporterTests.m */, 3360843C2C340C76008CC412 /* RNSentryBreadcrumbTests.swift */, - 3360843B2C340C75008CC412 /* RNSentryCocoaTesterTests-Bridging-Header.h */, ); path = RNSentryCocoaTesterTests; sourceTree = ""; @@ -97,6 +98,7 @@ 33AFE0122B8F319000AAB120 /* RNSentry */ = { isa = PBXGroup; children = ( + 33BB05E02CF4C77C00EE1DD1 /* RNSentryReplay.h */, 3360843A2C32E3A8008CC412 /* RNSentryReplayBreadcrumbConverter.h */, 330F308D2C0F385A002A0D4E /* RNSentryBreadcrumb.h */, 33958C672BFCEF5A00AD1FB6 /* RNSentryOnDrawReporter.h */, @@ -217,9 +219,9 @@ files = ( 33AFDFF12B8D15E500AAB120 /* RNSentryDependencyContainerTests.m in Sources */, 336084392C32E382008CC412 /* RNSentryReplayBreadcrumbConverterTests.swift in Sources */, + 33BB05DF2CF4C72400EE1DD1 /* RNSentryReplayOptionsTests.swift in Sources */, 33F58AD02977037D008F60EA /* RNSentryTests.mm in Sources */, 33958C692BFCF12600AD1FB6 /* RNSentryOnDrawReporterTests.m in Sources */, - 3360843D2C340C76008CC412 /* RNSentryBreadcrumbTests.swift in Sources */, 33AFDFED2B8D14B300AAB120 /* RNSentryFramesTrackerListenerTests.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryCocoaTesterTests-Bridging-Header.h b/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryCocoaTesterTests-Bridging-Header.h index e177d453fb..e9197ce889 100644 --- a/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryCocoaTesterTests-Bridging-Header.h +++ b/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryCocoaTesterTests-Bridging-Header.h @@ -4,3 +4,4 @@ #import "RNSentryReplayBreadcrumbConverter.h" #import "RNSentryBreadcrumb.h" +#import "RNSentryReplay.h" diff --git a/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryReplayOptionsTests.swift b/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryReplayOptionsTests.swift new file mode 100644 index 0000000000..de21603af8 --- /dev/null +++ b/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryReplayOptionsTests.swift @@ -0,0 +1,185 @@ +import XCTest +import Sentry + +final class RNSentryReplayOptions: XCTestCase { + + func testOptionsWithoutExperimentalAreIgnored() { + let optionsDict = NSMutableDictionary() + RNSentryReplay.updateOptions(optionsDict) + + XCTAssertEqual(optionsDict.count, 0) + } + + func testExperimentalOptionsWithoutReplaySampleRatesAreRemoved() { + let optionsDict = (["_experiments": [:]] as NSDictionary).mutableCopy() as! NSMutableDictionary + RNSentryReplay.updateOptions(optionsDict) + + XCTAssertEqual(optionsDict.count, 0) + } + + func testReplayOptionsDictContainsAllOptionsKeysWhenSessionSampleRateUsed() { + let optionsDict = ([ + "dsn": "https://abc@def.ingest.sentry.io/1234567", + "_experiments": [ + "replaysSessionSampleRate": 0.75 + ] + ] as NSDictionary).mutableCopy() as! NSMutableDictionary + RNSentryReplay.updateOptions(optionsDict) + + let experimental = optionsDict["experimental"] as! [String:Any] + let sessionReplay = experimental["sessionReplay"] as! [String:Any] + + assertAllDefaultReplayOptionsAreNotNil(replayOptions: sessionReplay) + } + + func testReplayOptionsDictContainsAllOptionsKeysWhenErrorSampleRateUsed() { + let optionsDict = ([ + "dsn": "https://abc@def.ingest.sentry.io/1234567", + "_experiments": [ + "replaysOnErrorSampleRate": 0.75 + ] + ] as NSDictionary).mutableCopy() as! NSMutableDictionary + RNSentryReplay.updateOptions(optionsDict) + + let experimental = optionsDict["experimental"] as! [String:Any] + let sessionReplay = experimental["sessionReplay"] as! [String:Any] + + assertAllDefaultReplayOptionsAreNotNil(replayOptions: sessionReplay) + } + + func testReplayOptionsDictContainsAllOptionsKeysWhenErrorAndSessionSampleRatesUsed() { + let optionsDict = ([ + "dsn": "https://abc@def.ingest.sentry.io/1234567", + "_experiments": [ + "replaysOnErrorSampleRate": 0.75, + "replaysSessionSampleRate": 0.75 + ] + ] as NSDictionary).mutableCopy() as! NSMutableDictionary + RNSentryReplay.updateOptions(optionsDict) + + let experimental = optionsDict["experimental"] as! [String:Any] + let sessionReplay = experimental["sessionReplay"] as! [String:Any] + + assertAllDefaultReplayOptionsAreNotNil(replayOptions: sessionReplay) + } + + func assertAllDefaultReplayOptionsAreNotNil(replayOptions: [String: Any]) { + XCTAssertEqual(replayOptions.count, 5) + XCTAssertNotNil(replayOptions["sessionSampleRate"]) + XCTAssertNotNil(replayOptions["errorSampleRate"]) + XCTAssertNotNil(replayOptions["maskAllImages"]) + XCTAssertNotNil(replayOptions["maskAllText"]) + XCTAssertNotNil(replayOptions["maskedViewClasses"]) + } + + func testSessionSampleRate() { + let optionsDict = ([ + "dsn": "https://abc@def.ingest.sentry.io/1234567", + "_experiments": [ "replaysSessionSampleRate": 0.75 ] + ] as NSDictionary).mutableCopy() as! NSMutableDictionary + RNSentryReplay.updateOptions(optionsDict) + + let actualOptions = try! Options(dict: optionsDict as! [String: Any]) + XCTAssertEqual(actualOptions.experimental.sessionReplay.sessionSampleRate, 0.75) + } + + func testOnErrorSampleRate() { + let optionsDict = ([ + "dsn": "https://abc@def.ingest.sentry.io/1234567", + "_experiments": [ "replaysOnErrorSampleRate": 0.75 ] + ] as NSDictionary).mutableCopy() as! NSMutableDictionary + RNSentryReplay.updateOptions(optionsDict) + + let actualOptions = try! Options(dict: optionsDict as! [String: Any]) + XCTAssertEqual(actualOptions.experimental.sessionReplay.onErrorSampleRate, 0.75) + } + + func testMaskAllVectors() { + let optionsDict = ([ + "dsn": "https://abc@def.ingest.sentry.io/1234567", + "_experiments": [ "replaysOnErrorSampleRate": 0.75 ], + "mobileReplayOptions": [ "maskAllVectors": true ] + ] as NSDictionary).mutableCopy() as! NSMutableDictionary + + RNSentryReplay.updateOptions(optionsDict) + + XCTAssertEqual(optionsDict.count, 3) + + let experimental = optionsDict["experimental"] as! [String:Any] + let sessionReplay = experimental["sessionReplay"] as! [String:Any] + + let maskedViewClasses = sessionReplay["maskedViewClasses"] as! [String] + XCTAssertEqual(maskedViewClasses.count, 1) + XCTAssertEqual(maskedViewClasses[0], "RNSVGSvgView") + + let actualOptions = try! Options(dict: optionsDict as! [String: Any]) + XCTAssertEqual(actualOptions.experimental.sessionReplay.maskedViewClasses.count, 0) + } + + func testMaskAllImages() { + let optionsDict = ([ + "dsn": "https://abc@def.ingest.sentry.io/1234567", + "_experiments": [ "replaysOnErrorSampleRate": 0.75 ], + "mobileReplayOptions": [ "maskAllImages": true ] + ] as NSDictionary).mutableCopy() as! NSMutableDictionary + + RNSentryReplay.updateOptions(optionsDict) + + let actualOptions = try! Options(dict: optionsDict as! [String: Any]) + + XCTAssertEqual(actualOptions.experimental.sessionReplay.maskAllImages, true) + XCTAssertEqual(actualOptions.experimental.sessionReplay.maskedViewClasses.count, 1) + XCTAssertNotNil(actualOptions.experimental.sessionReplay.maskedViewClasses[0]) + XCTAssertEqual(ObjectIdentifier(actualOptions.experimental.sessionReplay.maskedViewClasses[0]), ObjectIdentifier(NSClassFromString("RCTImageView")!)) + } + + func testMaskAllImagesFalse() { + let optionsDict = ([ + "dsn": "https://abc@def.ingest.sentry.io/1234567", + "_experiments": [ "replaysOnErrorSampleRate": 0.75 ], + "mobileReplayOptions": [ "maskAllImages": false ] + ] as NSDictionary).mutableCopy() as! NSMutableDictionary + + RNSentryReplay.updateOptions(optionsDict) + + let actualOptions = try! Options(dict: optionsDict as! [String: Any]) + + XCTAssertEqual(actualOptions.experimental.sessionReplay.maskAllImages, false) + XCTAssertEqual(actualOptions.experimental.sessionReplay.maskedViewClasses.count, 0) + } + + func testMaskAllText() { + let optionsDict = ([ + "dsn": "https://abc@def.ingest.sentry.io/1234567", + "_experiments": [ "replaysOnErrorSampleRate": 0.75 ], + "mobileReplayOptions": [ "maskAllText": true ] + ] as NSDictionary).mutableCopy() as! NSMutableDictionary + + RNSentryReplay.updateOptions(optionsDict) + + let actualOptions = try! Options(dict: optionsDict as! [String: Any]) + + XCTAssertEqual(actualOptions.experimental.sessionReplay.maskAllText, true) + XCTAssertEqual(actualOptions.experimental.sessionReplay.maskedViewClasses.count, 2) + XCTAssertNotNil(actualOptions.experimental.sessionReplay.maskedViewClasses[0]) + XCTAssertNotNil(actualOptions.experimental.sessionReplay.maskedViewClasses[1]) + XCTAssertEqual(ObjectIdentifier(actualOptions.experimental.sessionReplay.maskedViewClasses[0]), ObjectIdentifier(NSClassFromString("RCTTextView")!)) + XCTAssertEqual(ObjectIdentifier(actualOptions.experimental.sessionReplay.maskedViewClasses[1]), ObjectIdentifier(NSClassFromString("RCTParagraphComponentView")!)) + } + + func testMaskAllTextFalse() { + let optionsDict = ([ + "dsn": "https://abc@def.ingest.sentry.io/1234567", + "_experiments": [ "replaysOnErrorSampleRate": 0.75 ], + "mobileReplayOptions": [ "maskAllText": false ] + ] as NSDictionary).mutableCopy() as! NSMutableDictionary + + RNSentryReplay.updateOptions(optionsDict) + + let actualOptions = try! Options(dict: optionsDict as! [String: Any]) + + XCTAssertEqual(actualOptions.experimental.sessionReplay.maskAllText, false) + XCTAssertEqual(actualOptions.experimental.sessionReplay.maskedViewClasses.count, 0) + } + +} diff --git a/ios/RNSentryReplay.m b/ios/RNSentryReplay.m index de6a0c240e..cd54bde6d7 100644 --- a/ios/RNSentryReplay.m +++ b/ios/RNSentryReplay.m @@ -29,40 +29,27 @@ + (void)updateOptions:(NSMutableDictionary *)options { ?: [NSNull null], @"errorSampleRate" : experiments[@"replaysOnErrorSampleRate"] ?: [NSNull null], - @"redactAllImages" : replayOptions[@"maskAllImages"] ?: [NSNull null], - @"redactAllText" : replayOptions[@"maskAllText"] ?: [NSNull null], + @"maskAllImages" : replayOptions[@"maskAllImages"] ?: [NSNull null], + @"maskAllText" : replayOptions[@"maskAllText"] ?: [NSNull null], + @"maskedViewClasses" : [RNSentryReplay getReplayRNRedactClasses:replayOptions], } } forKey:@"experimental"]; - - [RNSentryReplay addReplayRNRedactClasses:replayOptions]; } -+ (void)addReplayRNRedactClasses:(NSDictionary *_Nullable)replayOptions { ++ (NSArray *_Nonnull)getReplayRNRedactClasses:(NSDictionary *_Nullable)replayOptions { NSMutableArray *_Nonnull classesToRedact = [[NSMutableArray alloc] init]; if ([replayOptions[@"maskAllVectors"] boolValue] == YES) { - Class _Nullable maybeRNSVGViewClass = NSClassFromString(@"RNSVGSvgView"); - if (maybeRNSVGViewClass != nil) { - [classesToRedact addObject:maybeRNSVGViewClass]; - } + [classesToRedact addObject:@"RNSVGSvgView"]; } if ([replayOptions[@"maskAllImages"] boolValue] == YES) { - Class _Nullable maybeRCTImageClass = NSClassFromString(@"RCTImageView"); - if (maybeRCTImageClass != nil) { - [classesToRedact addObject:maybeRCTImageClass]; - } + [classesToRedact addObject:@"RCTImageView"]; } if ([replayOptions[@"maskAllText"] boolValue] == YES) { - Class _Nullable maybeRCTTextClass = NSClassFromString(@"RCTTextView"); - if (maybeRCTTextClass != nil) { - [classesToRedact addObject:maybeRCTTextClass]; - } - Class _Nullable maybeRCTParagraphComponentViewClass = NSClassFromString(@"RCTParagraphComponentView"); - if (maybeRCTParagraphComponentViewClass != nil) { - [classesToRedact addObject:maybeRCTParagraphComponentViewClass]; - } + [classesToRedact addObject:@"RCTTextView"]; + [classesToRedact addObject:@"RCTParagraphComponentView"]; } - [PrivateSentrySDKOnly addReplayRedactClasses:classesToRedact]; + return classesToRedact; } + (void)postInit {