From 117d38b6bd9b230a83a3c5217a782cba5589dd9a Mon Sep 17 00:00:00 2001 From: Krystof Woldrich <31292499+krystofwoldrich@users.noreply.github.com> Date: Tue, 19 Nov 2024 12:51:55 +0100 Subject: [PATCH 01/11] fix(replay): `browserReplayIntegration` should not be included by default (#4270) --- CHANGELOG.md | 6 +++ src/js/integrations/default.ts | 6 ++- test/sdk.test.ts | 96 ++++++++++++++++++++++++++++++++++ 3 files changed, 106 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0856bda6d5..b6dd3546c9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## Unreleased + +### 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)) + ## 5.35.0 ### Fixes diff --git a/src/js/integrations/default.ts b/src/js/integrations/default.ts index bcbac5c2cb..29369b3dad 100644 --- a/src/js/integrations/default.ts +++ b/src/js/integrations/default.ts @@ -10,7 +10,6 @@ import { browserApiErrorsIntegration, browserGlobalHandlersIntegration, browserLinkedErrorsIntegration, - browserReplayIntegration, debugSymbolicatorIntegration, dedupeIntegration, deviceContextIntegration, @@ -120,10 +119,13 @@ export function getDefaultIntegrations(options: ReactNativeClientOptions): Integ (options._experiments && typeof options._experiments.replaysOnErrorSampleRate === 'number') || (options._experiments && typeof options._experiments.replaysSessionSampleRate === 'number') ) { - integrations.push(notWeb() ? mobileReplayIntegration() : browserReplayIntegration()); if (!notWeb()) { + // We can't create and add browserReplayIntegration as it overrides the users supplied one + // The browser replay integration works differently than the rest of default integrations (options as BrowserOptions).replaysOnErrorSampleRate = options._experiments.replaysOnErrorSampleRate; (options as BrowserOptions).replaysSessionSampleRate = options._experiments.replaysSessionSampleRate; + } else { + integrations.push(mobileReplayIntegration()); } } diff --git a/test/sdk.test.ts b/test/sdk.test.ts index 0593ba1667..a007cb7953 100644 --- a/test/sdk.test.ts +++ b/test/sdk.test.ts @@ -648,6 +648,102 @@ describe('Tests the SDK functionality', () => { expect(actualIntegrations).toEqual(expect.arrayContaining([expect.objectContaining({ name: 'ExpoContext' })])); }); + + it('adds mobile replay integration when _experiments.replaysOnErrorSampleRate is set', () => { + init({ + _experiments: { + replaysOnErrorSampleRate: 1.0, + }, + }); + + const actualOptions = usedOptions(); + const actualIntegrations = actualOptions?.integrations; + expect(actualIntegrations).toEqual(expect.arrayContaining([expect.objectContaining({ name: 'MobileReplay' })])); + }); + + it('adds mobile replay integration when _experiments.replaysSessionSampleRate is set', () => { + init({ + _experiments: { + replaysSessionSampleRate: 1.0, + }, + }); + + const actualOptions = usedOptions(); + const actualIntegrations = actualOptions?.integrations; + expect(actualIntegrations).toEqual(expect.arrayContaining([expect.objectContaining({ name: 'MobileReplay' })])); + }); + + it('does not add mobile replay integration when no replay sample rates are set', () => { + init({ + _experiments: {}, + }); + + const actualOptions = usedOptions(); + const actualIntegrations = actualOptions?.integrations; + expect(actualIntegrations).toEqual(expect.not.arrayContaining([expect.objectContaining({ name: 'MobileReplay' })])); + }); + + it('does not add any replay integration when on web even with on error sample rate', () => { + (notWeb as jest.Mock).mockImplementation(() => false); + init({ + _experiments: { + replaysOnErrorSampleRate: 1.0, + }, + }); + + const actualOptions = usedOptions(); + const actualIntegrations = actualOptions?.integrations; + expect(actualIntegrations).toEqual(expect.not.arrayContaining([expect.objectContaining({ name: 'Replay' })])); + expect(actualIntegrations).toEqual( + expect.not.arrayContaining([expect.objectContaining({ name: 'MobileReplay' })]), + ); + }); + + it('does not add any replay integration when on web even with session sample rate', () => { + (notWeb as jest.Mock).mockImplementation(() => false); + init({ + _experiments: { + replaysSessionSampleRate: 1.0, + }, + }); + + const actualOptions = usedOptions(); + const actualIntegrations = actualOptions?.integrations; + expect(actualIntegrations).toEqual(expect.not.arrayContaining([expect.objectContaining({ name: 'Replay' })])); + expect(actualIntegrations).toEqual( + expect.not.arrayContaining([expect.objectContaining({ name: 'MobileReplay' })]), + ); + }); + + it('does not add any replay integration when on web', () => { + (notWeb as jest.Mock).mockImplementation(() => false); + init({}); + + const actualOptions = usedOptions(); + const actualIntegrations = actualOptions?.integrations; + expect(actualIntegrations).toEqual(expect.not.arrayContaining([expect.objectContaining({ name: 'Replay' })])); + expect(actualIntegrations).toEqual( + expect.not.arrayContaining([expect.objectContaining({ name: 'MobileReplay' })]), + ); + }); + + it('converts experimental replay options to standard web options when on web', () => { + (notWeb as jest.Mock).mockImplementation(() => false); + init({ + _experiments: { + replaysOnErrorSampleRate: 0.5, + replaysSessionSampleRate: 0.1, + }, + }); + + const actualOptions = usedOptions(); + expect(actualOptions).toEqual( + expect.objectContaining({ + replaysOnErrorSampleRate: 0.5, + replaysSessionSampleRate: 0.1, + }), + ); + }); }); function createMockedIntegration({ name }: { name?: string } = {}): Integration { From 58226d9ee4b4f230ba589e73655111a702e8de64 Mon Sep 17 00:00:00 2001 From: Krystof Woldrich Date: Mon, 25 Nov 2024 16:18:11 +0100 Subject: [PATCH 02/11] fix(replay): Backport iOS masking fixes from RN SDK v6 --- CHANGELOG.md | 9 + RNSentry.podspec | 2 +- .../project.pbxproj | 10 +- ...RNSentryCocoaTesterTests-Bridging-Header.h | 1 + .../RNSentryReplayOptionsTests.swift | 185 ++++++++++++++++++ ios/RNSentryReplay.m | 31 +-- 6 files changed, 211 insertions(+), 27 deletions(-) create mode 100644 RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryReplayOptionsTests.swift diff --git a/CHANGELOG.md b/CHANGELOG.md index b6dd3546c9..5d95d43408 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,16 @@ ### 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)) +- 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 { From 99c3cd9adb732f162154c46803c6ddbf6b54f903 Mon Sep 17 00:00:00 2001 From: Krystof Woldrich Date: Mon, 25 Nov 2024 17:55:15 +0100 Subject: [PATCH 03/11] empty, trigger CI in PR --- .github/workflows/e2e.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 656492517e..114f943ded 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -7,6 +7,7 @@ on: - v5 pull_request: + concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true From f0f316ca213373e160714b352113f6230d63bf6d Mon Sep 17 00:00:00 2001 From: Krystof Woldrich <31292499+krystofwoldrich@users.noreply.github.com> Date: Mon, 2 Dec 2024 14:46:39 +0100 Subject: [PATCH 04/11] Update client.ts --- src/js/client.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/js/client.ts b/src/js/client.ts index 278c9e0cb2..b9e954e1cd 100644 --- a/src/js/client.ts +++ b/src/js/client.ts @@ -26,6 +26,7 @@ import { ignoreRequireCycleLogs } from './utils/ignorerequirecyclelogs'; import { mergeOutcomes } from './utils/outcome'; import { NATIVE } from './wrapper'; + /** * The Sentry React Native SDK Client. * From 444a356ed43482490ffbae4715d334b4d25c61c5 Mon Sep 17 00:00:00 2001 From: Krystof Woldrich Date: Mon, 2 Dec 2024 15:19:16 +0100 Subject: [PATCH 05/11] fix ci --- .github/workflows/e2e.yml | 18 ++++++------------ .github/workflows/native-tests.yml | 4 ++-- .github/workflows/sample-application.yml | 9 ++++----- 3 files changed, 12 insertions(+), 19 deletions(-) diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 114f943ded..db580edb75 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -14,6 +14,8 @@ concurrency: env: SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} + IOS_DEVICE: 'iPhone 16' + IOS_RUNTIME: '18.1' jobs: diff_check: @@ -43,7 +45,7 @@ jobs: platform: ["ios", "android"] include: - platform: ios - runs-on: macos-12 + runs-on: macos-13 name: iOS appPlain: test/perf/test-app-plain.ipa - platform: android @@ -188,13 +190,9 @@ jobs: - platform: ios rn-version: '0.73.9' runs-on: macos-14 # uses m1 https://github.blog/changelog/2024-01-30-github-actions-macos-14-sonoma-is-now-available/ - runtime: 'latest' - device: 'iPhone 14' - platform: ios rn-version: '0.65.3' - runs-on: macos-12 - runtime: 'latest' - device: 'iPhone 14' + runs-on: macos-13 - platform: android runs-on: ubuntu-latest exclude: @@ -345,14 +343,10 @@ jobs: include: - platform: ios rn-version: '0.73.9' - runs-on: macos-14 # uses m1 https://github.blog/changelog/2024-01-30-github-actions-macos-14-sonoma-is-now-available/ - runtime: 'latest' - device: 'iPhone 14' + runs-on: macos-15 - platform: ios rn-version: '0.65.3' - runs-on: macos-latest - runtime: 'latest' - device: 'iPhone 14' + runs-on: macos-15 - platform: android runs-on: ubuntu-latest exclude: diff --git a/.github/workflows/native-tests.yml b/.github/workflows/native-tests.yml index f296aeea1c..0451941410 100644 --- a/.github/workflows/native-tests.yml +++ b/.github/workflows/native-tests.yml @@ -23,7 +23,7 @@ jobs: test-ios: name: ios - runs-on: macos-14 # uses m1 https://github.blog/changelog/2024-01-30-github-actions-macos-14-sonoma-is-now-available/ + runs-on: macos-15 needs: [diff_check] if: ${{ needs.diff_check.outputs.skip_ci != 'true' }} steps: @@ -49,7 +49,7 @@ jobs: env: SCHEME: RNSentryCocoaTester CONFIGURATION: Release - DESTINATION: 'platform=iOS Simulator,OS=latest,name=iPhone 14' + DESTINATION: 'platform=iOS Simulator,OS=latest,name=iPhone 16' run: | env NSUnbufferedIO=YES \ xcodebuild -workspace *.xcworkspace \ diff --git a/.github/workflows/sample-application.yml b/.github/workflows/sample-application.yml index b104938a5e..6775b30f7a 100644 --- a/.github/workflows/sample-application.yml +++ b/.github/workflows/sample-application.yml @@ -46,11 +46,9 @@ jobs: build-type: ['dev', 'production'] include: - platform: ios - runs-on: macos-14 # uses m1 https://github.blog/changelog/2024-01-30-github-actions-macos-14-sonoma-is-now-available/ - runtime: 'latest' - device: 'iPhone 14' + runs-on: macos-15 - platform: macos - runs-on: macos-14 + runs-on: macos-15 - platform: android runs-on: ubuntu-latest exclude: @@ -143,7 +141,8 @@ jobs: -workspace sentryreactnativesample.xcworkspace \ -configuration "$CONFIG" \ -scheme sentryreactnativesample \ - -destination 'platform=iOS Simulator,OS=${{ matrix.runtime }},name=${{ matrix.device }}' \ + -sdk 'iphonesimulator' \ + -destination 'generic/platform=iOS Simulator' \ ONLY_ACTIVE_ARCH=yes \ -derivedDataPath "$derivedData" \ build \ From 51e8d5b354169cfabb4163c6fbe73a6e60f55238 Mon Sep 17 00:00:00 2001 From: Krystof Woldrich <31292499+krystofwoldrich@users.noreply.github.com> Date: Mon, 2 Dec 2024 15:39:35 +0100 Subject: [PATCH 06/11] Update e2e.mjs --- scripts/e2e.mjs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/e2e.mjs b/scripts/e2e.mjs index e73ecb105b..1c2943664d 100755 --- a/scripts/e2e.mjs +++ b/scripts/e2e.mjs @@ -181,7 +181,8 @@ if (actions.includes('test')) { execSync(`set -o pipefail && xcodebuild \ -project node_modules/appium-webdriveragent/WebDriverAgent.xcodeproj \ -scheme WebDriverAgentRunner \ - -destination 'platform=iOS Simulator,OS=${runtime},name=${device}' \ + -sdk 'iphonesimulator' \ + -destination 'generic/platform=iOS Simulator' \ GCC_TREAT_WARNINGS_AS_ERRORS=0 \ COMPILER_INDEX_STORE_ENABLE=NO \ ONLY_ACTIVE_ARCH=yes \ From 5cf4c45aa12ad45bdedb60eb697d85e520027653 Mon Sep 17 00:00:00 2001 From: Krystof Woldrich <31292499+krystofwoldrich@users.noreply.github.com> Date: Mon, 2 Dec 2024 15:43:35 +0100 Subject: [PATCH 07/11] Update e2e.yml --- .github/workflows/e2e.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index db580edb75..9f0b1723fd 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -7,7 +7,6 @@ on: - v5 pull_request: - concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true From d573b008c184abda5566912cef9e4733e6b7f353 Mon Sep 17 00:00:00 2001 From: Krystof Woldrich <31292499+krystofwoldrich@users.noreply.github.com> Date: Mon, 2 Dec 2024 15:44:13 +0100 Subject: [PATCH 08/11] Update client.ts --- src/js/client.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/js/client.ts b/src/js/client.ts index b9e954e1cd..278c9e0cb2 100644 --- a/src/js/client.ts +++ b/src/js/client.ts @@ -26,7 +26,6 @@ import { ignoreRequireCycleLogs } from './utils/ignorerequirecyclelogs'; import { mergeOutcomes } from './utils/outcome'; import { NATIVE } from './wrapper'; - /** * The Sentry React Native SDK Client. * From 083468964af6fc9e87cea961a602ea804284bfab Mon Sep 17 00:00:00 2001 From: Krystof Woldrich <31292499+krystofwoldrich@users.noreply.github.com> Date: Mon, 2 Dec 2024 15:45:52 +0100 Subject: [PATCH 09/11] Update e2e.yml --- .github/workflows/e2e.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 9f0b1723fd..cc133a8467 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -174,8 +174,6 @@ jobs: USE_FRAMEWORKS: ${{ matrix.ios-use-frameworks }} PRODUCTION: ${{ matrix.build-type == 'production' && '1' || '0' }} RCT_NEW_ARCH_ENABLED: ${{ matrix.rn-architecture == 'new' && '1' || '0' }} - IOS_RUNTIME: ${{ matrix.runtime }} - IOS_DEVICE: ${{ matrix.device }} strategy: fail-fast: false # keeps matrix running if one fails matrix: @@ -363,7 +361,6 @@ jobs: rn-architecture: 'new' env: PLATFORM: ${{ matrix.platform }} - DEVICE: ${{ matrix.device }} steps: - uses: actions/checkout@v4 From 5aec52933ee083af11d025f9c77566ce466368a5 Mon Sep 17 00:00:00 2001 From: Krystof Woldrich <31292499+krystofwoldrich@users.noreply.github.com> Date: Mon, 2 Dec 2024 16:07:32 +0100 Subject: [PATCH 10/11] Update e2e.mjs --- scripts/e2e.mjs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/e2e.mjs b/scripts/e2e.mjs index 1c2943664d..16bb11903e 100755 --- a/scripts/e2e.mjs +++ b/scripts/e2e.mjs @@ -152,7 +152,8 @@ if (actions.includes('build')) { -workspace ${appName}.xcworkspace \ -configuration ${buildType} \ -scheme ${appName} \ - -destination 'platform=iOS Simulator,OS=${runtime},name=${device}' \ + -sdk 'iphonesimulator' \ + -destination 'generic/platform=iOS Simulator' \ ONLY_ACTIVE_ARCH=yes \ -derivedDataPath DerivedData \ build | tee xcodebuild.log | xcbeautify`, From f5f711d1fed111c2a14e31455c682b304242655f Mon Sep 17 00:00:00 2001 From: Krystof Woldrich Date: Mon, 2 Dec 2024 17:49:13 +0100 Subject: [PATCH 11/11] fix lint --- test/sdk.test.ts | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/test/sdk.test.ts b/test/sdk.test.ts index a007cb7953..9921289a0e 100644 --- a/test/sdk.test.ts +++ b/test/sdk.test.ts @@ -694,9 +694,7 @@ describe('Tests the SDK functionality', () => { const actualOptions = usedOptions(); const actualIntegrations = actualOptions?.integrations; expect(actualIntegrations).toEqual(expect.not.arrayContaining([expect.objectContaining({ name: 'Replay' })])); - expect(actualIntegrations).toEqual( - expect.not.arrayContaining([expect.objectContaining({ name: 'MobileReplay' })]), - ); + expect(actualIntegrations).toEqual(expect.not.arrayContaining([expect.objectContaining({ name: 'MobileReplay' })])); }); it('does not add any replay integration when on web even with session sample rate', () => { @@ -710,9 +708,7 @@ describe('Tests the SDK functionality', () => { const actualOptions = usedOptions(); const actualIntegrations = actualOptions?.integrations; expect(actualIntegrations).toEqual(expect.not.arrayContaining([expect.objectContaining({ name: 'Replay' })])); - expect(actualIntegrations).toEqual( - expect.not.arrayContaining([expect.objectContaining({ name: 'MobileReplay' })]), - ); + expect(actualIntegrations).toEqual(expect.not.arrayContaining([expect.objectContaining({ name: 'MobileReplay' })])); }); it('does not add any replay integration when on web', () => { @@ -722,9 +718,7 @@ describe('Tests the SDK functionality', () => { const actualOptions = usedOptions(); const actualIntegrations = actualOptions?.integrations; expect(actualIntegrations).toEqual(expect.not.arrayContaining([expect.objectContaining({ name: 'Replay' })])); - expect(actualIntegrations).toEqual( - expect.not.arrayContaining([expect.objectContaining({ name: 'MobileReplay' })]), - ); + expect(actualIntegrations).toEqual(expect.not.arrayContaining([expect.objectContaining({ name: 'MobileReplay' })])); }); it('converts experimental replay options to standard web options when on web', () => {