From 8aec30ebd6b60262f8b935b5475741c92da38834 Mon Sep 17 00:00:00 2001 From: Bruno Garcia Date: Tue, 30 Apr 2024 16:07:03 -0400 Subject: [PATCH 01/12] date range to license - 2015-2024 (#3921) --- LICENSE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE.md b/LICENSE.md index 5a483f25178..95b14dbf956 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2015 Sentry +Copyright (c) 2015-2024 Sentry Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal From ca507eccc67597d1f2c0645c8f84942879046b32 Mon Sep 17 00:00:00 2001 From: Philipp Hofmann Date: Fri, 3 May 2024 14:15:37 +0200 Subject: [PATCH 02/12] chore: Format code with clang-format 18.1.5 (#3935) --- .../Recording/Monitors/SentryCrashMonitorType.c | 5 +---- .../Recording/Tools/SentryCrashSignalInfo.c | 15 +++------------ 2 files changed, 4 insertions(+), 16 deletions(-) diff --git a/Sources/SentryCrash/Recording/Monitors/SentryCrashMonitorType.c b/Sources/SentryCrash/Recording/Monitors/SentryCrashMonitorType.c index b14aead36d9..f40c7327ada 100644 --- a/Sources/SentryCrash/Recording/Monitors/SentryCrashMonitorType.c +++ b/Sources/SentryCrash/Recording/Monitors/SentryCrashMonitorType.c @@ -31,10 +31,7 @@ static const struct { const SentryCrashMonitorType type; const char *const name; } g_monitorTypes[] = { -#define MONITORTYPE(NAME) \ - { \ - NAME, #NAME \ - } +#define MONITORTYPE(NAME) { NAME, #NAME } MONITORTYPE(SentryCrashMonitorTypeMachException), MONITORTYPE(SentryCrashMonitorTypeSignal), MONITORTYPE(SentryCrashMonitorTypeCPPException), diff --git a/Sources/SentryCrash/Recording/Tools/SentryCrashSignalInfo.c b/Sources/SentryCrash/Recording/Tools/SentryCrashSignalInfo.c index 8d27ed32597..c52897917b1 100644 --- a/Sources/SentryCrash/Recording/Tools/SentryCrashSignalInfo.c +++ b/Sources/SentryCrash/Recording/Tools/SentryCrashSignalInfo.c @@ -42,10 +42,7 @@ typedef struct { const int numCodes; } SentryCrashSignalInfo; -#define ENUM_NAME_MAPPING(A) \ - { \ - A, #A \ - } +#define ENUM_NAME_MAPPING(A) { A, #A } static const SentryCrashSignalCodeInfo g_sigIllCodes[] = { #ifdef ILL_NOOP @@ -98,14 +95,8 @@ static const SentryCrashSignalCodeInfo g_sigSegVCodes[] = { ENUM_NAME_MAPPING(SEGV_ACCERR), }; -#define SIGNAL_INFO(SIGNAL, CODES) \ - { \ - SIGNAL, #SIGNAL, CODES, sizeof(CODES) / sizeof(*CODES) \ - } -#define SIGNAL_INFO_NOCODES(SIGNAL) \ - { \ - SIGNAL, #SIGNAL, 0, 0 \ - } +#define SIGNAL_INFO(SIGNAL, CODES) { SIGNAL, #SIGNAL, CODES, sizeof(CODES) / sizeof(*CODES) } +#define SIGNAL_INFO_NOCODES(SIGNAL) { SIGNAL, #SIGNAL, 0, 0 } static const SentryCrashSignalInfo g_fatalSignalData[] = { SIGNAL_INFO_NOCODES(SIGABRT), From 32ac9341b30033766e7ff03cd2e29592ad90e6ff Mon Sep 17 00:00:00 2001 From: Philipp Hofmann Date: Fri, 3 May 2024 14:50:23 +0200 Subject: [PATCH 03/12] impr: Remove not needed log for logging (#3934) Every log message needs to acquire a lock to evaluate whether the logger should log it. #3303 added this logic to fix a data race the thread sanitizer job found. As the SDK only calls the configure method when starting, we don't need to put a synchronized keyword around the code that evaluates the log level. This PR replaces the synchronized keyword by ignoring the thread sanitizer. --- CHANGELOG.md | 4 ++++ Sources/Sentry/SentryLog.m | 9 ++++++--- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4d663ae37ab..090f1fa4138 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ - Ignore SentryFramesTracker thread sanitizer data races (#3922) - Handle no releaseName in WatchDogTerminationLogic (#3919) +### Improvements + +- Remove not needed lock for logging (#3934) + ## 8.25.0 ### Features diff --git a/Sources/Sentry/SentryLog.m b/Sources/Sentry/SentryLog.m index 4335fc67fd0..d53d219a57b 100644 --- a/Sources/Sentry/SentryLog.m +++ b/Sources/Sentry/SentryLog.m @@ -1,4 +1,5 @@ #import "SentryLog.h" +#import "SentryInternalCDefines.h" #import "SentryLevelMapper.h" #import "SentryLogOutput.h" @@ -37,10 +38,12 @@ + (void)logWithMessage:(NSString *)message andLevel:(SentryLevel)level } + (BOOL)willLogAtLevel:(SentryLevel)level + SENTRY_DISABLE_THREAD_SANITIZER( + "The SDK usually configures the log level and isDebug once when it starts. For tests, we " + "accept a data race causing some log messages of the wrong level over using a synchronized " + "block for this method, as it's called frequently in production.") { - @synchronized(logConfigureLock) { - return isDebug && level != kSentryLevelNone && level >= diagnosticLevel; - } + return isDebug && level != kSentryLevelNone && level >= diagnosticLevel; } // Internal and only needed for testing. From 6d541bc49b4dcaa9e965a6e7c0e5fc539284b638 Mon Sep 17 00:00:00 2001 From: Andrew McKnight Date: Sun, 5 May 2024 16:19:27 -0500 Subject: [PATCH 04/12] test: compile sectors for test/testci for debug also (#3929) --- Sentry.xcodeproj/project.pbxproj | 4 ---- Sources/Configuration/SDK.xcconfig | 8 ++++++++ Sources/Sentry/SentryAppStartTracker.m | 8 ++++---- Sources/Sentry/SentryHttpTransport.m | 12 +++++------ Sources/Sentry/SentryNSProcessInfoWrapper.mm | 4 ++-- Sources/Sentry/SentryReachability.m | 20 +++++++++---------- Sources/Sentry/SentrySpotlightTransport.m | 4 ++-- Sources/Sentry/SentrySwizzle.m | 4 ++-- Sources/Sentry/SentryTracer.m | 4 ++-- .../include/HybridPublic/SentrySwizzle.h | 8 ++++---- .../include/SentryNSProcessInfoWrapper.h | 4 ++-- Sources/Sentry/include/SentryReachability.h | 8 ++++---- Sources/Sentry/include/SentryTransport.h | 4 ++-- .../Recording/SentryCrashBinaryImageCache.c | 4 ++-- .../SessionReplay/SentryOnDemandReplay.swift | 4 ++-- 15 files changed, 52 insertions(+), 48 deletions(-) diff --git a/Sentry.xcodeproj/project.pbxproj b/Sentry.xcodeproj/project.pbxproj index 716ee9b912e..61b2c5e47a6 100644 --- a/Sentry.xcodeproj/project.pbxproj +++ b/Sentry.xcodeproj/project.pbxproj @@ -5569,7 +5569,6 @@ OTHER_SWIFT_FLAGS = "-DCARTHAGE"; PRODUCT_BUNDLE_IDENTIFIER = io.sentry.SentrySwiftUI; SKIP_INSTALL = YES; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_INCLUDE_PATHS = Sources/SentrySwiftUI/; SWIFT_OBJC_BRIDGING_HEADER = ""; @@ -6300,7 +6299,6 @@ OTHER_SWIFT_FLAGS = "-DCARTHAGE"; PRODUCT_BUNDLE_IDENTIFIER = io.sentry.SentrySwiftUI; SKIP_INSTALL = YES; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_INCLUDE_PATHS = Sources/SentrySwiftUI/; SWIFT_OBJC_BRIDGING_HEADER = ""; @@ -6496,7 +6494,6 @@ PROVISIONING_PROFILE_SPECIFIER = ""; SDKROOT = iphoneos; SKIP_INSTALL = YES; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; @@ -6552,7 +6549,6 @@ PROVISIONING_PROFILE_SPECIFIER = ""; SDKROOT = iphoneos; SKIP_INSTALL = YES; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; diff --git a/Sources/Configuration/SDK.xcconfig b/Sources/Configuration/SDK.xcconfig index d092a97c74b..fccf5ae1380 100644 --- a/Sources/Configuration/SDK.xcconfig +++ b/Sources/Configuration/SDK.xcconfig @@ -32,3 +32,11 @@ CLANG_CXX_LIBRARY = libc++ // leads to an error Module _SentryPrivate not found in the SentryTests-Swift.h header when import the // SentryPrivate module with @import _SentryPrivate. HEADER_SEARCH_PATHS = $(SRCROOT)/Sources/Sentry/include/** + +SWIFT_ACTIVE_COMPILATION_CONDITIONS_Debug = DEBUG +SWIFT_ACTIVE_COMPILATION_CONDITIONS_Debug_without_UIKit = DEBUG +SWIFT_ACTIVE_COMPILATION_CONDITIONS_Test = TEST +SWIFT_ACTIVE_COMPILATION_CONDITIONS_TestCI = TESTCI +SWIFT_ACTIVE_COMPILATION_CONDITIONS_Release = +SWIFT_ACTIVE_COMPILATION_CONDITIONS_Release_without_UIKit = +SWIFT_ACTIVE_COMPILATION_CONDITIONS = $(SWIFT_ACTIVE_COMPILATION_CONDITIONS_$(CONFIGURATION)) diff --git a/Sources/Sentry/SentryAppStartTracker.m b/Sources/Sentry/SentryAppStartTracker.m index 3170b275257..e9f8d84ade2 100644 --- a/Sources/Sentry/SentryAppStartTracker.m +++ b/Sources/Sentry/SentryAppStartTracker.m @@ -226,12 +226,12 @@ - (void)buildAppStartMeasurement:(NSDate *)appStartEnd // With only running this once we know that the process is a new one when the following // code is executed. // We need to make sure the block runs on each test instead of only once -# if TEST +# if defined(TEST) || defined(TESTCI) || defined(DEBUG) block(); # else static dispatch_once_t once; [self.dispatchQueue dispatchOnce:&once block:block]; -# endif +# endif // defined(TEST) || defined(TESTCI) || defined(DEBUG) } /** @@ -316,9 +316,9 @@ - (void)stop [self.framesTracker removeListener:self]; -# if TEST +# if defined(TEST) || defined(TESTCI) || defined(DEBUG) self.isRunning = NO; -# endif +# endif // defined(TEST) || defined(TESTCI) || defined(DEBUG) } - (void)dealloc diff --git a/Sources/Sentry/SentryHttpTransport.m b/Sources/Sentry/SentryHttpTransport.m index 2c60867da1b..a9a8a6d8ba1 100644 --- a/Sources/Sentry/SentryHttpTransport.m +++ b/Sources/Sentry/SentryHttpTransport.m @@ -42,9 +42,9 @@ @property (nonatomic, strong) SentryDispatchQueueWrapper *dispatchQueue; @property (nonatomic, strong) dispatch_group_t dispatchGroup; -#if TEST || TESTCI +#if defined(TEST) || defined(TESTCI) || defined(DEBUG) @property (nullable, nonatomic, strong) void (^startFlushCallback)(void); -#endif +#endif // defined(TEST) || defined(TESTCI) || defined(DEBUG) /** * Relay expects the discarded events split by data category and reason; see @@ -161,12 +161,12 @@ - (void)recordLostEvent:(SentryDataCategory)category reason:(SentryDiscardReason } } -#if TEST || TESTCI +#if defined(TEST) || defined(TESTCI) || defined(DEBUG) - (void)setStartFlushCallback:(void (^)(void))callback { _startFlushCallback = callback; } -#endif +#endif // defined(TEST) || defined(TESTCI) || defined(DEBUG) - (SentryFlushResult)flush:(NSTimeInterval)timeout { @@ -192,11 +192,11 @@ - (SentryFlushResult)flush:(NSTimeInterval)timeout _isFlushing = YES; dispatch_group_enter(self.dispatchGroup); -#if TEST || TESTCI +#if defined(TEST) || defined(TESTCI) || defined(DEBUG) if (self.startFlushCallback != nil) { self.startFlushCallback(); } -#endif +#endif // defined(TEST) || defined(TESTCI) || defined(DEBUG) } [self sendAllCachedEnvelopes]; diff --git a/Sources/Sentry/SentryNSProcessInfoWrapper.mm b/Sources/Sentry/SentryNSProcessInfoWrapper.mm index 87b289041e0..bf7713e5f88 100644 --- a/Sources/Sentry/SentryNSProcessInfoWrapper.mm +++ b/Sources/Sentry/SentryNSProcessInfoWrapper.mm @@ -1,7 +1,7 @@ #import "SentryNSProcessInfoWrapper.h" @implementation SentryNSProcessInfoWrapper { -#if TEST +#if defined(TEST) || defined(TESTCI) || defined(DEBUG) NSString *_executablePath; } - (void)setProcessPath:(NSString *)path @@ -20,7 +20,7 @@ - (instancetype)init #else } # define SENTRY_BINARY_EXECUTABLE_PATH NSBundle.mainBundle.executablePath; -#endif +#endif // defined(TEST) || defined(TESTCI) || defined(DEBUG) + (SentryNSProcessInfoWrapper *)shared { diff --git a/Sources/Sentry/SentryReachability.m b/Sources/Sentry/SentryReachability.m index d50d46a508d..3d5aca8f17f 100644 --- a/Sources/Sentry/SentryReachability.m +++ b/Sources/Sentry/SentryReachability.m @@ -39,7 +39,7 @@ NSString *const SentryConnectivityWiFi = @"wifi"; NSString *const SentryConnectivityNone = @"none"; -# if TEST || TESTCI +# if defined(TEST) || defined(TESTCI) || defined(DEBUG) static BOOL sentry_reachability_ignore_actual_callback = NO; void @@ -48,7 +48,7 @@ SENTRY_LOG_DEBUG(@"Setting ignore actual callback to %@", value ? @"YES" : @"NO"); sentry_reachability_ignore_actual_callback = value; } -# endif // TEST || TESTCI +# endif // defined(TEST) || defined(TESTCI) || defined(DEBUG) /** * Check whether the connectivity change should be noted or ignored. @@ -135,12 +135,12 @@ { SENTRY_LOG_DEBUG( @"SentryConnectivityCallback called with target: %@; flags: %u", target, flags); -# if TEST || TESTCI +# if defined(TEST) || defined(TESTCI) || defined(DEBUG) if (sentry_reachability_ignore_actual_callback) { SENTRY_LOG_DEBUG(@"Ignoring actual callback."); return; } -# endif // TEST || TESTCI +# endif // defined(TEST) || defined(TESTCI) || defined(DEBUG) SentryConnectivityCallback(flags); } @@ -161,7 +161,7 @@ + (void)initialize } } -# if TEST || TESTCI +# if defined(TEST) || defined(TESTCI) || defined(DEBUG) - (instancetype)init { @@ -172,7 +172,7 @@ - (instancetype)init return self; } -# endif // TEST || TESTCI +# endif // defined(TEST) || defined(TESTCI) || defined(DEBUG) - (void)addObserver:(id)observer; { @@ -190,12 +190,12 @@ - (void)addObserver:(id)observer; return; } -# if TEST || TESTCI +# if defined(TEST) || defined(TESTCI) || defined(DEBUG) if (self.skipRegisteringActualCallbacks) { SENTRY_LOG_DEBUG(@"Skip registering actual callbacks"); return; } -# endif // TEST || TESTCI +# endif // defined(TEST) || defined(TESTCI) || defined(DEBUG) sentry_reachability_queue = dispatch_queue_create("io.sentry.cocoa.connectivity", DISPATCH_QUEUE_SERIAL); @@ -240,11 +240,11 @@ - (void)removeAllObservers - (void)unsetReachabilityCallback { -# if TEST || TESTCI +# if defined(TEST) || defined(TESTCI) || defined(DEBUG) if (self.skipRegisteringActualCallbacks) { SENTRY_LOG_DEBUG(@"Skip unsetting actual callbacks"); } -# endif // TEST || TESTCI +# endif // defined(TEST) || defined(TESTCI) || defined(DEBUG) sentry_current_reachability_state = kSCNetworkReachabilityFlagsUninitialized; diff --git a/Sources/Sentry/SentrySpotlightTransport.m b/Sources/Sentry/SentrySpotlightTransport.m index a4e3754c077..7f528bed8fb 100644 --- a/Sources/Sentry/SentrySpotlightTransport.m +++ b/Sources/Sentry/SentrySpotlightTransport.m @@ -96,12 +96,12 @@ - (void)recordLostEvent:(SentryDataCategory)category reason:(SentryDiscardReason // Empty on purpose } -#if TEST || TESTCI +#if defined(TEST) || defined(TESTCI) || defined(DEBUG) - (void)setStartFlushCallback:(nonnull void (^)(void))callback { // Empty on purpose } -#endif +#endif // defined(TEST) || defined(TESTCI) || defined(DEBUG) @end diff --git a/Sources/Sentry/SentrySwizzle.m b/Sources/Sentry/SentrySwizzle.m index 2874d1f76cb..516492a34d5 100644 --- a/Sources/Sentry/SentrySwizzle.m +++ b/Sources/Sentry/SentrySwizzle.m @@ -25,11 +25,11 @@ - (SentrySwizzleOriginalIMP)getOriginalImplementation return NULL; } -#if TEST +#if defined(TEST) || defined(TESTCI) || defined(DEBUG) @synchronized(self) { self.originalCalled = YES; } -#endif +#endif // defined(TEST) || defined(TESTCI) || defined(DEBUG) // Casting IMP to SentrySwizzleOriginalIMP to force user casting. return (SentrySwizzleOriginalIMP)_impProviderBlock(); diff --git a/Sources/Sentry/SentryTracer.m b/Sources/Sentry/SentryTracer.m index 5145392c2e7..50d2ecaf491 100644 --- a/Sources/Sentry/SentryTracer.m +++ b/Sources/Sentry/SentryTracer.m @@ -50,10 +50,10 @@ # import #endif // SENTRY_HAS_UIKIT -#if defined(TEST) || defined(TESTCI) +#if defined(TEST) || defined(TESTCI) || defined(DEBUG) # import "SentryFileManager+Test.h" # import "SentryInternalDefines.h" -#endif // defined(TEST) || defined(TESTCI) +#endif // defined(TEST) || defined(TESTCI) || defined(DEBUG) NS_ASSUME_NONNULL_BEGIN diff --git a/Sources/Sentry/include/HybridPublic/SentrySwizzle.h b/Sources/Sentry/include/HybridPublic/SentrySwizzle.h index d0d8f5a7cb0..6a824167417 100644 --- a/Sources/Sentry/include/HybridPublic/SentrySwizzle.h +++ b/Sources/Sentry/include/HybridPublic/SentrySwizzle.h @@ -159,12 +159,12 @@ typedef void (*SentrySwizzleOriginalIMP)(void /* id, SEL, ... */); */ @property (nonatomic, readonly) SEL selector; -#if TEST +#if defined(TEST) || defined(TESTCI) || defined(DEBUG) /** * A flag to check whether the original implementation was called. */ @property (nonatomic) BOOL originalCalled; -#endif +#endif // defined(TEST) || defined(TESTCI) || defined(DEBUG) @end @@ -367,7 +367,7 @@ typedef NS_ENUM(NSUInteger, SentrySwizzleMode) { // and remove it later. #define _SentrySWArguments(arguments...) DEL, ##arguments -#if TEST +#if defined(TEST) || defined(TESTCI) || defined(DEBUG) # define _SentrySWReplacement(code...) \ @try { \ code \ @@ -379,7 +379,7 @@ typedef NS_ENUM(NSUInteger, SentrySwizzleMode) { } #else # define _SentrySWReplacement(code...) code -#endif +#endif // defined(TEST) || defined(TESTCI) || defined(DEBUG) #define _SentrySwizzleInstanceMethod(classToSwizzle, selector, SentrySWReturnType, \ SentrySWArguments, SentrySWReplacement, SentrySwizzleMode, KEY) \ diff --git a/Sources/Sentry/include/SentryNSProcessInfoWrapper.h b/Sources/Sentry/include/SentryNSProcessInfoWrapper.h index ab3be20a061..5bd38f6fc3d 100644 --- a/Sources/Sentry/include/SentryNSProcessInfoWrapper.h +++ b/Sources/Sentry/include/SentryNSProcessInfoWrapper.h @@ -8,9 +8,9 @@ NS_ASSUME_NONNULL_BEGIN @property (nullable, nonatomic, readonly) NSString *processPath; @property (readonly) NSUInteger processorCount; -#if TEST +#if defined(TEST) || defined(TESTCI) || defined(DEBUG) - (void)setProcessPath:(NSString *)path; -#endif +#endif // defined(TEST) || defined(TESTCI) || defined(DEBUG) @end diff --git a/Sources/Sentry/include/SentryReachability.h b/Sources/Sentry/include/SentryReachability.h index a93c20fd8b4..585d511d7a5 100644 --- a/Sources/Sentry/include/SentryReachability.h +++ b/Sources/Sentry/include/SentryReachability.h @@ -34,13 +34,13 @@ NS_ASSUME_NONNULL_BEGIN void SentryConnectivityCallback(SCNetworkReachabilityFlags flags); -# if TEST || TESTCI +# if defined(TEST) || defined(TESTCI) || defined(DEBUG) /** * Needed for testing. */ void SentrySetReachabilityIgnoreActualCallback(BOOL value); -# endif // TEST || TESTCI +# endif // defined(TEST) || defined(TESTCI) || defined(DEBUG) NSString *SentryConnectivityFlagRepresentation(SCNetworkReachabilityFlags flags); @@ -68,7 +68,7 @@ SENTRY_EXTERN NSString *const SentryConnectivityNone; */ @interface SentryReachability : NSObject -# if TEST || TESTCI +# if defined(TEST) || defined(TESTCI) || defined(DEBUG) /** * Only needed for testing. Use this flag to skip registering and unregistering the actual callbacks @@ -76,7 +76,7 @@ SENTRY_EXTERN NSString *const SentryConnectivityNone; */ @property (nonatomic, assign) BOOL skipRegisteringActualCallbacks; -# endif // TEST || TESTCI +# endif // defined(TEST) || defined(TESTCI) || defined(DEBUG) /** * Add an observer which is called each time network connectivity changes. diff --git a/Sources/Sentry/include/SentryTransport.h b/Sources/Sentry/include/SentryTransport.h index 76fbd1a813d..340562d4d15 100644 --- a/Sources/Sentry/include/SentryTransport.h +++ b/Sources/Sentry/include/SentryTransport.h @@ -21,9 +21,9 @@ NS_SWIFT_NAME(Transport) - (SentryFlushResult)flush:(NSTimeInterval)timeout; -#if TEST || TESTCI +#if defined(TEST) || defined(TESTCI) || defined(DEBUG) - (void)setStartFlushCallback:(void (^)(void))callback; -#endif +#endif // defined(TEST) || defined(TESTCI) || defined(DEBUG) @end diff --git a/Sources/SentryCrash/Recording/SentryCrashBinaryImageCache.c b/Sources/SentryCrash/Recording/SentryCrashBinaryImageCache.c index 9e412937151..62212e614dd 100644 --- a/Sources/SentryCrash/Recording/SentryCrashBinaryImageCache.c +++ b/Sources/SentryCrash/Recording/SentryCrashBinaryImageCache.c @@ -7,7 +7,7 @@ #include #include -#if TEST || TESTCI +#if defined(TEST) || defined(TESTCI) || defined(DEBUG) typedef void (*SentryRegisterImageCallback)(const struct mach_header *mh, intptr_t vmaddr_slide); typedef void (*SentryRegisterFunction)(SentryRegisterImageCallback function); @@ -57,7 +57,7 @@ sentry_resetFuncForAddRemoveImage(void) # define sentry_dyld_register_func_for_remove_image(CALLBACK) \ _dyld_register_func_for_remove_image(CALLBACK) # define _will_add_image() -#endif +#endif // defined(TEST) || defined(TESTCI) || defined(DEBUG) typedef struct SentryCrashBinaryImageNode { SentryCrashBinaryImage image; diff --git a/Sources/Swift/Integrations/SessionReplay/SentryOnDemandReplay.swift b/Sources/Swift/Integrations/SessionReplay/SentryOnDemandReplay.swift index d56db06b02e..dd343b8e50b 100644 --- a/Sources/Swift/Integrations/SessionReplay/SentryOnDemandReplay.swift +++ b/Sources/Swift/Integrations/SessionReplay/SentryOnDemandReplay.swift @@ -30,13 +30,13 @@ class SentryOnDemandReplay: NSObject { private let workingQueue: SentryDispatchQueueWrapper private var _frames = [SentryReplayFrame]() - #if TEST + #if TEST || TESTCI || DEBUG //This is exposed only for tests, no need to make it thread safe. var frames: [SentryReplayFrame] { get { _frames } set { _frames = newValue } } - #endif + #endif // TEST || TESTCI || DEBUG var videoWidth = 200 var videoHeight = 434 From c379c5e4a8b08af6cfec9b4380ca97a45dc333b9 Mon Sep 17 00:00:00 2001 From: Philipp Hofmann Date: Mon, 6 May 2024 12:15:00 +0200 Subject: [PATCH 05/12] chore: Move XCFrameworks to root after building (#3766) Move XCFrameworks zip files to the repository root folder after building so that you can compile the Carthage sample locally. --- .github/workflows/build.yml | 1 + scripts/create-carthage-json.sh | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a86689c0ead..4589ad56069 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -149,6 +149,7 @@ jobs: - uses: actions/download-artifact@v4 with: name: ${{ github.sha }} + path: Carthage/ - run: ./scripts/ci-select-xcode.sh 15.2 - run: make build-xcframework-sample shell: sh diff --git a/scripts/create-carthage-json.sh b/scripts/create-carthage-json.sh index 25430ea9f8c..1d45034ba6d 100755 --- a/scripts/create-carthage-json.sh +++ b/scripts/create-carthage-json.sh @@ -1,5 +1,5 @@ #!/bin/bash set -euo pipefail -echo "{ \"1.0\": \"file:///$(pwd)/Sentry.framework.zip?alt=file:///$(pwd)/Sentry.xcframework.zip\" }" > ./Samples/Carthage-Validation/Sentry.Carthage.json -echo "{ \"1.0\": \"file:///$(pwd)/SentrySwiftUI.framework.zip?alt=file:///$(pwd)/SentrySwiftUI.xcframework.zip\" }" > ./Samples/Carthage-Validation/SentrySwiftUI.Carthage.json +echo "{ \"1.0\": \"file:///$(pwd)/Carthage/Sentry.framework.zip?alt=file:///$(pwd)/Carthage/Sentry.xcframework.zip\" }" > ./Samples/Carthage-Validation/Sentry.Carthage.json +echo "{ \"1.0\": \"file:///$(pwd)/Carthage/SentrySwiftUI.framework.zip?alt=file:///$(pwd)/Carthage/SentrySwiftUI.xcframework.zip\" }" > ./Samples/Carthage-Validation/SentrySwiftUI.Carthage.json From 6a7328fc00310f329cdab0963794f1c70f615da4 Mon Sep 17 00:00:00 2001 From: Dhiogo Brustolin Date: Mon, 6 May 2024 16:04:15 +0200 Subject: [PATCH 06/12] ref: Use average color for redact masking (#3877) Use the text color or the average color of the image as the redaction mask. Co-authored-by: Philipp Hofmann --- Samples/iOS-Swift/iOS-Swift/AppDelegate.swift | 2 +- Sentry.xcodeproj/project.pbxproj | 36 +++- Sources/Sentry/SentryCoreGraphicsHelper.m | 18 -- Sources/Sentry/SentrySessionReplay.m | 35 ++-- .../Sentry/SentrySessionReplayIntegration.m | 11 -- .../Sentry/include/SentryCoreGraphicsHelper.h | 13 -- Sources/Sentry/include/SentryPrivate.h | 1 - Sources/Sentry/include/SentrySessionReplay.h | 22 +-- .../SessionReplay/SentryOnDemandReplay.swift | 2 +- .../SentryReplayVideoMaker.swift | 11 ++ .../Swift/Tools/SentryViewPhotographer.swift | 122 +++---------- .../Tools/SentryViewScreenshotProvider.swift | 13 ++ Sources/Swift/Tools/UIImageHelper.swift | 36 ++++ Sources/Swift/Tools/UIRedactBuilder.swift | 139 +++++++++++++++ .../SentrySessionReplayTests.swift | 31 ++-- Tests/SentryTests/RedactRegionTests.swift | 143 ++++++++++++++++ Tests/SentryTests/UIImageHelperTests.swift | 65 +++++++ Tests/SentryTests/UIRedactBuilderTests.swift | 162 ++++++++++++++++++ 18 files changed, 658 insertions(+), 204 deletions(-) delete mode 100644 Sources/Sentry/SentryCoreGraphicsHelper.m delete mode 100644 Sources/Sentry/include/SentryCoreGraphicsHelper.h create mode 100644 Sources/Swift/Integrations/SessionReplay/SentryReplayVideoMaker.swift create mode 100644 Sources/Swift/Tools/SentryViewScreenshotProvider.swift create mode 100644 Sources/Swift/Tools/UIImageHelper.swift create mode 100644 Sources/Swift/Tools/UIRedactBuilder.swift create mode 100644 Tests/SentryTests/RedactRegionTests.swift create mode 100644 Tests/SentryTests/UIImageHelperTests.swift create mode 100644 Tests/SentryTests/UIRedactBuilderTests.swift diff --git a/Samples/iOS-Swift/iOS-Swift/AppDelegate.swift b/Samples/iOS-Swift/iOS-Swift/AppDelegate.swift index 03d38b423b9..40135715ec2 100644 --- a/Samples/iOS-Swift/iOS-Swift/AppDelegate.swift +++ b/Samples/iOS-Swift/iOS-Swift/AppDelegate.swift @@ -25,7 +25,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { options.debug = true if #available(iOS 16.0, *) { - options.experimental.sessionReplay = SentryReplayOptions(sessionSampleRate: 0, errorSampleRate: 1, redactAllText: true, redactAllImages: true) + options.experimental.sessionReplay = SentryReplayOptions(sessionSampleRate: 1, errorSampleRate: 1, redactAllText: true, redactAllImages: true) } if #available(iOS 15.0, *) { diff --git a/Sentry.xcodeproj/project.pbxproj b/Sentry.xcodeproj/project.pbxproj index 61b2c5e47a6..d8b8e46c741 100644 --- a/Sentry.xcodeproj/project.pbxproj +++ b/Sentry.xcodeproj/project.pbxproj @@ -795,7 +795,6 @@ D820CDB42BB1886100BA339D /* SentrySessionReplay.h in Headers */ = {isa = PBXBuildFile; fileRef = D820CDB12BB1886100BA339D /* SentrySessionReplay.h */; }; D820CDB72BB1895F00BA339D /* SentrySessionReplayIntegration.m in Sources */ = {isa = PBXBuildFile; fileRef = D820CDB62BB1895F00BA339D /* SentrySessionReplayIntegration.m */; }; D820CDB82BB1895F00BA339D /* SentrySessionReplayIntegration.h in Headers */ = {isa = PBXBuildFile; fileRef = D820CDB52BB1895F00BA339D /* SentrySessionReplayIntegration.h */; }; - D820CE132BB2F13C00BA339D /* SentryCoreGraphicsHelper.h in Headers */ = {isa = PBXBuildFile; fileRef = D820CE112BB2F13C00BA339D /* SentryCoreGraphicsHelper.h */; }; D8292D7D2A39A027009872F7 /* UrlSanitizedTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8292D7C2A39A027009872F7 /* UrlSanitizedTests.swift */; }; D8370B6A273DF1E900F66E2D /* SentryNSURLSessionTaskSearch.m in Sources */ = {isa = PBXBuildFile; fileRef = D8370B68273DF1E900F66E2D /* SentryNSURLSessionTaskSearch.m */; }; D8370B6C273DF20F00F66E2D /* SentryNSURLSessionTaskSearch.h in Headers */ = {isa = PBXBuildFile; fileRef = D8370B6B273DF20F00F66E2D /* SentryNSURLSessionTaskSearch.h */; }; @@ -838,7 +837,6 @@ D86F419827C8FEFA00490520 /* SentryCoreDataTrackerExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = D86F419727C8FEFA00490520 /* SentryCoreDataTrackerExtension.swift */; }; D8751FA5274743710032F4DE /* SentryNSURLSessionTaskSearchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8751FA4274743710032F4DE /* SentryNSURLSessionTaskSearchTests.swift */; }; D875ED0B276CC84700422FAC /* SentryNSDataTrackerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D875ED0A276CC84700422FAC /* SentryNSDataTrackerTests.swift */; }; - D878C6A82BC7F01C0039D6A3 /* SentryCoreGraphicsHelper.m in Sources */ = {isa = PBXBuildFile; fileRef = D820CE122BB2F13C00BA339D /* SentryCoreGraphicsHelper.m */; }; D87C89032BC43C9C0086C7DF /* SentryRedactOptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D87C89022BC43C9C0086C7DF /* SentryRedactOptions.swift */; }; D87C892B2BC67BC20086C7DF /* SentryExperimentalOptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D87C892A2BC67BC20086C7DF /* SentryExperimentalOptions.swift */; }; D880E3A728573E87008A90DB /* SentryBaggageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D880E3A628573E87008A90DB /* SentryBaggageTests.swift */; }; @@ -859,6 +857,10 @@ D8ACE3CE2762187D00F5A213 /* SentryNSDataTracker.h in Headers */ = {isa = PBXBuildFile; fileRef = D8ACE3CB2762187D00F5A213 /* SentryNSDataTracker.h */; }; D8ACE3CF2762187D00F5A213 /* SentryFileIOTrackingIntegration.h in Headers */ = {isa = PBXBuildFile; fileRef = D8ACE3CC2762187D00F5A213 /* SentryFileIOTrackingIntegration.h */; }; D8AFC0012BD252B900118BE1 /* SentryOnDemandReplayTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8AFC0002BD252B900118BE1 /* SentryOnDemandReplayTests.swift */; }; + D8AFC01A2BD7A20B00118BE1 /* SentryViewScreenshotProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8AFC0192BD7A20B00118BE1 /* SentryViewScreenshotProvider.swift */; }; + D8AFC03D2BDA79BF00118BE1 /* SentryReplayVideoMaker.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8AFC03C2BDA79BF00118BE1 /* SentryReplayVideoMaker.swift */; }; + D8AFC0572BDA895400118BE1 /* UIRedactBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8AFC0562BDA895400118BE1 /* UIRedactBuilder.swift */; }; + D8AFC05A2BDA89C100118BE1 /* RedactRegionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8AFC0582BDA899A00118BE1 /* RedactRegionTests.swift */; }; D8AFC0622BDBEE4200118BE1 /* SentrySessionReplayIntegration+Private.h in Headers */ = {isa = PBXBuildFile; fileRef = D8AFC0612BDBEDF100118BE1 /* SentrySessionReplayIntegration+Private.h */; }; D8B0542E2A7D2C720056BAF6 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = D8B0542D2A7D2C720056BAF6 /* PrivacyInfo.xcprivacy */; }; D8B088B629C9E3FF00213258 /* SentryTracerConfiguration.h in Headers */ = {isa = PBXBuildFile; fileRef = D8B088B429C9E3FF00213258 /* SentryTracerConfiguration.h */; }; @@ -890,6 +892,9 @@ D8CE69BC277E39C700C6EC5C /* SentryFileIOTrackingIntegrationObjCTests.m in Sources */ = {isa = PBXBuildFile; fileRef = D8CE69BB277E39C700C6EC5C /* SentryFileIOTrackingIntegrationObjCTests.m */; }; D8F016B32B9622D6007B9AFB /* SentryId.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8F016B22B9622D6007B9AFB /* SentryId.swift */; }; D8F016B62B962548007B9AFB /* StringExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8F016B52B962548007B9AFB /* StringExtensions.swift */; }; + D8F67AEE2BE0D19200C9197B /* UIImageHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8F67AED2BE0D19200C9197B /* UIImageHelper.swift */; }; + D8F67AF12BE0D33F00C9197B /* UIImageHelperTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8F67AEF2BE0D31A00C9197B /* UIImageHelperTests.swift */; }; + D8F67AF42BE10F9600C9197B /* UIRedactBuilderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8F67AF22BE10F7600C9197B /* UIRedactBuilderTests.swift */; }; D8F6A2472885512100320515 /* SentryPredicateDescriptor.m in Sources */ = {isa = PBXBuildFile; fileRef = D8F6A2452885512100320515 /* SentryPredicateDescriptor.m */; }; D8F6A24B2885515C00320515 /* SentryPredicateDescriptor.h in Headers */ = {isa = PBXBuildFile; fileRef = D8F6A24A2885515B00320515 /* SentryPredicateDescriptor.h */; }; D8F6A24E288553A800320515 /* SentryPredicateDescriptorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8F6A24C2885534E00320515 /* SentryPredicateDescriptorTests.swift */; }; @@ -1819,8 +1824,6 @@ D820CDB22BB1886100BA339D /* SentrySessionReplay.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentrySessionReplay.m; sourceTree = ""; }; D820CDB52BB1895F00BA339D /* SentrySessionReplayIntegration.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SentrySessionReplayIntegration.h; path = include/SentrySessionReplayIntegration.h; sourceTree = ""; }; D820CDB62BB1895F00BA339D /* SentrySessionReplayIntegration.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentrySessionReplayIntegration.m; sourceTree = ""; }; - D820CE112BB2F13C00BA339D /* SentryCoreGraphicsHelper.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SentryCoreGraphicsHelper.h; path = include/SentryCoreGraphicsHelper.h; sourceTree = ""; }; - D820CE122BB2F13C00BA339D /* SentryCoreGraphicsHelper.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryCoreGraphicsHelper.m; sourceTree = ""; }; D8292D7A2A38AF04009872F7 /* HTTPHeaderSanitizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTPHeaderSanitizer.swift; sourceTree = ""; }; D8292D7C2A39A027009872F7 /* UrlSanitizedTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UrlSanitizedTests.swift; sourceTree = ""; }; D8370B68273DF1E900F66E2D /* SentryNSURLSessionTaskSearch.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryNSURLSessionTaskSearch.m; sourceTree = ""; }; @@ -1890,6 +1893,10 @@ D8ACE3CB2762187D00F5A213 /* SentryNSDataTracker.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SentryNSDataTracker.h; path = include/SentryNSDataTracker.h; sourceTree = ""; }; D8ACE3CC2762187D00F5A213 /* SentryFileIOTrackingIntegration.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SentryFileIOTrackingIntegration.h; path = include/SentryFileIOTrackingIntegration.h; sourceTree = ""; }; D8AFC0002BD252B900118BE1 /* SentryOnDemandReplayTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryOnDemandReplayTests.swift; sourceTree = ""; }; + D8AFC0192BD7A20B00118BE1 /* SentryViewScreenshotProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryViewScreenshotProvider.swift; sourceTree = ""; }; + D8AFC03C2BDA79BF00118BE1 /* SentryReplayVideoMaker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryReplayVideoMaker.swift; sourceTree = ""; }; + D8AFC0562BDA895400118BE1 /* UIRedactBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIRedactBuilder.swift; sourceTree = ""; }; + D8AFC0582BDA899A00118BE1 /* RedactRegionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RedactRegionTests.swift; sourceTree = ""; }; D8AFC0612BDBEDF100118BE1 /* SentrySessionReplayIntegration+Private.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "SentrySessionReplayIntegration+Private.h"; path = "include/SentrySessionReplayIntegration+Private.h"; sourceTree = ""; }; D8B0542D2A7D2C720056BAF6 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = ""; }; D8B088B429C9E3FF00213258 /* SentryTracerConfiguration.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SentryTracerConfiguration.h; path = include/SentryTracerConfiguration.h; sourceTree = ""; }; @@ -1927,6 +1934,9 @@ D8F016B52B962548007B9AFB /* StringExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StringExtensions.swift; sourceTree = ""; }; D8F01DE42A126B62008F4996 /* HybridPod.podspec */ = {isa = PBXFileReference; explicitFileType = text.script.ruby; path = HybridPod.podspec; sourceTree = ""; }; D8F01DE52A126BF5008F4996 /* HybridTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HybridTest.swift; sourceTree = ""; }; + D8F67AED2BE0D19200C9197B /* UIImageHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIImageHelper.swift; sourceTree = ""; }; + D8F67AEF2BE0D31A00C9197B /* UIImageHelperTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIImageHelperTests.swift; sourceTree = ""; }; + D8F67AF22BE10F7600C9197B /* UIRedactBuilderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIRedactBuilderTests.swift; sourceTree = ""; }; D8F6A2452885512100320515 /* SentryPredicateDescriptor.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryPredicateDescriptor.m; sourceTree = ""; }; D8F6A24A2885515B00320515 /* SentryPredicateDescriptor.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SentryPredicateDescriptor.h; path = include/SentryPredicateDescriptor.h; sourceTree = ""; }; D8F6A24C2885534E00320515 /* SentryPredicateDescriptorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryPredicateDescriptorTests.swift; sourceTree = ""; }; @@ -3543,8 +3553,6 @@ D820CDB52BB1895F00BA339D /* SentrySessionReplayIntegration.h */, D8AFC0612BDBEDF100118BE1 /* SentrySessionReplayIntegration+Private.h */, D820CDB62BB1895F00BA339D /* SentrySessionReplayIntegration.m */, - D820CE112BB2F13C00BA339D /* SentryCoreGraphicsHelper.h */, - D820CE122BB2F13C00BA339D /* SentryCoreGraphicsHelper.m */, ); name = SessionReplay; sourceTree = ""; @@ -3583,6 +3591,9 @@ D8292D7C2A39A027009872F7 /* UrlSanitizedTests.swift */, D8F8F5562B835BC600AC5465 /* SentryMsgPackSerializerTests.m */, D8B425112B9A0FD6000BFDF3 /* StringExtensionTests.swift */, + D8AFC0582BDA899A00118BE1 /* RedactRegionTests.swift */, + D8F67AEF2BE0D31A00C9197B /* UIImageHelperTests.swift */, + D8F67AF22BE10F7600C9197B /* UIRedactBuilderTests.swift */, ); name = Tools; sourceTree = ""; @@ -3611,6 +3622,9 @@ D856272B2A374A8600FB8062 /* UrlSanitized.swift */, D8292D7A2A38AF04009872F7 /* HTTPHeaderSanitizer.swift */, D8CAC0722BA4473000E38F34 /* SentryViewPhotographer.swift */, + D8AFC0192BD7A20B00118BE1 /* SentryViewScreenshotProvider.swift */, + D8AFC0562BDA895400118BE1 /* UIRedactBuilder.swift */, + D8F67AED2BE0D19200C9197B /* UIImageHelper.swift */, ); path = Tools; sourceTree = ""; @@ -3741,6 +3755,7 @@ D8CAC02B2BA0663E00E38F34 /* SentryVideoInfo.swift */, D802994D2BA836EF000F0081 /* SentryOnDemandReplay.swift */, D802994F2BA83A88000F0081 /* SentryPixelBuffer.swift */, + D8AFC03C2BDA79BF00118BE1 /* SentryReplayVideoMaker.swift */, ); path = SessionReplay; sourceTree = ""; @@ -3828,7 +3843,6 @@ 7B0A54222521C21E00A71716 /* SentryFrameRemover.h in Headers */, 63FE70CD20DA4C1000CDBAE8 /* SentryCrashDoctor.h in Headers */, D8C67E9B28000E24007E326E /* SentryUIApplication.h in Headers */, - D820CE132BB2F13C00BA339D /* SentryCoreGraphicsHelper.h in Headers */, 7B6438AA26A70F24000D0F65 /* UIViewController+Sentry.h in Headers */, 639FCFAC1EBC811400778193 /* SentryUser.h in Headers */, D8CB74192947285A00A5F964 /* SentryEnvelopeItemHeader.h in Headers */, @@ -4347,6 +4361,7 @@ 7BFC16A125249A9D00FF6266 /* SentryMessage.m in Sources */, 7BCFBD6F2681D0EE00BC27D8 /* SentryCrashScopeObserver.m in Sources */, 7BD86ED1264A7CF6005439DB /* SentryAppStartMeasurement.m in Sources */, + D8F67AEE2BE0D19200C9197B /* UIImageHelper.swift in Sources */, 7DC27EC723997EB7006998B5 /* SentryAutoBreadcrumbTrackingIntegration.m in Sources */, 63FE717B20DA4C1100CDBAE8 /* SentryCrashReport.c in Sources */, 7B7A599726B692F00060A676 /* SentryScreenFrames.m in Sources */, @@ -4420,6 +4435,7 @@ 7B18DE4228D9F794004845C6 /* SentryNSNotificationCenterWrapper.m in Sources */, 639FCFA91EBC80CC00778193 /* SentryFrame.m in Sources */, D858FA672A29EAB3002A3503 /* SentryBinaryImageCache.m in Sources */, + D8AFC0572BDA895400118BE1 /* UIRedactBuilder.swift in Sources */, 8E564AEA267AF22600FE117D /* SentryNetworkTracker.m in Sources */, 15360CED2433A15500112302 /* SentryInstallation.m in Sources */, 7B98D7E825FB7BCD00C5A389 /* SentryAppState.m in Sources */, @@ -4459,9 +4475,11 @@ 0A9BF4E228A114940068D266 /* SentryViewHierarchyIntegration.m in Sources */, 0ADC33EC28D9BB780078D980 /* SentryUIDeviceWrapper.m in Sources */, 7BBD188B244841FB00427C76 /* SentryHttpDateParser.m in Sources */, + D8AFC03D2BDA79BF00118BE1 /* SentryReplayVideoMaker.swift in Sources */, 840A11122B61E27500650D02 /* SentrySamplerDecision.m in Sources */, 8E4E7C8225DAB2A5006AB9E2 /* SentryTracer.m in Sources */, 848A45192BBF8D33006AAAEC /* SentryContinuousProfiler.m in Sources */, + D8AFC01A2BD7A20B00118BE1 /* SentryViewScreenshotProvider.swift in Sources */, 15E0A8E5240C457D00F044E3 /* SentryEnvelope.m in Sources */, 03F84D3627DD4191008FE43F /* SentryProfilingLogging.mm in Sources */, 8EC3AE7A25CA23B600E7591A /* SentrySpan.m in Sources */, @@ -4538,7 +4556,6 @@ 63FE712D20DA4C1100CDBAE8 /* SentryCrashJSONCodecObjC.m in Sources */, 7BBD18932449BEDD00427C76 /* SentryDefaultRateLimits.m in Sources */, 7BD729982463E93500EA3610 /* SentryDateUtil.m in Sources */, - D878C6A82BC7F01C0039D6A3 /* SentryCoreGraphicsHelper.m in Sources */, 62262B882BA1C490004DA3DD /* SentryStatsdClient.m in Sources */, 639FCF9D1EBC7F9500778193 /* SentryThread.m in Sources */, 8E8C57A225EEFC07001CEEFA /* SentrySampling.m in Sources */, @@ -4637,6 +4654,7 @@ 63FE722420DA66EC00CDBAE8 /* SentryCrashMonitor_NSException_Tests.m in Sources */, 7B5AB65D27E48E5200F1D1BA /* TestThreadInspector.swift in Sources */, 7BF9EF742722A85B00B5BBEF /* SentryClassRegistrator.m in Sources */, + D8F67AF42BE10F9600C9197B /* UIRedactBuilderTests.swift in Sources */, 63B819141EC352A7002FDF4C /* SentryInterfacesTests.m in Sources */, 7B68345128F7EB3D00FB7064 /* SentryMeasurementUnitTests.swift in Sources */, 7B14089A248791660035403D /* SentryCrashStackEntryMapperTests.swift in Sources */, @@ -4765,6 +4783,7 @@ 62BAD74E2BA1C58D00EBAAFC /* EncodeMetricTests.swift in Sources */, 7BE0DC29272A9E1C004FA8B7 /* SentryBreadcrumbTrackerTests.swift in Sources */, 63FE722520DA66EC00CDBAE8 /* SentryCrashFileUtils_Tests.m in Sources */, + D8AFC05A2BDA89C100118BE1 /* RedactRegionTests.swift in Sources */, D86130122BB563FD004C0F5E /* SentrySessionReplayIntegrationTests.swift in Sources */, 7BFC16BA2524D4AF00FF6266 /* SentryMessage+Equality.m in Sources */, 7B4260342630315C00B36EDD /* SampleError.swift in Sources */, @@ -4775,6 +4794,7 @@ D880E3A728573E87008A90DB /* SentryBaggageTests.swift in Sources */, 7B16FD022654F86B008177D3 /* SentrySysctlTests.swift in Sources */, 7BAF3DB5243C743E008A5414 /* SentryClientTests.swift in Sources */, + D8F67AF12BE0D33F00C9197B /* UIImageHelperTests.swift in Sources */, 8EAE8E5E2681768000D6958B /* URLSessionTaskMock.m in Sources */, D8CE69BC277E39C700C6EC5C /* SentryFileIOTrackingIntegrationObjCTests.m in Sources */, D85D3BEA278DF63D001B2889 /* SentryByteCountFormatterTests.swift in Sources */, diff --git a/Sources/Sentry/SentryCoreGraphicsHelper.m b/Sources/Sentry/SentryCoreGraphicsHelper.m deleted file mode 100644 index 56bb3816299..00000000000 --- a/Sources/Sentry/SentryCoreGraphicsHelper.m +++ /dev/null @@ -1,18 +0,0 @@ -#import "SentryCoreGraphicsHelper.h" -#if SENTRY_HAS_UIKIT -@implementation SentryCoreGraphicsHelper -+ (CGMutablePathRef)excludeRect:(CGRect)rectangle fromPath:(CGMutablePathRef)path -{ -# if (TARGET_OS_IOS || TARGET_OS_TV) -# ifdef __IPHONE_16_0 - if (@available(iOS 16.0, tvOS 16.0, *)) { - CGPathRef exclude = CGPathCreateWithRect(rectangle, nil); - CGPathRef newPath = CGPathCreateCopyBySubtractingPath(path, exclude, YES); - return CGPathCreateMutableCopy(newPath); - } -# endif // defined(__IPHONE_16_0) -# endif // (TARGET_OS_IOS || TARGET_OS_TV) - return path; -} -@end -#endif // SENTRY_HAS_UIKIT diff --git a/Sources/Sentry/SentrySessionReplay.m b/Sources/Sentry/SentrySessionReplay.m index a31659ebb99..428cc2fdd67 100644 --- a/Sources/Sentry/SentrySessionReplay.m +++ b/Sources/Sentry/SentrySessionReplay.m @@ -35,7 +35,7 @@ @implementation SentrySessionReplay { NSDate *_sessionStart; NSMutableArray *imageCollection; SentryReplayOptions *_replayOptions; - SentryOnDemandReplay *_replayMaker; + id _replayMaker; SentryDisplayLinkWrapper *_displayLink; SentryCurrentDateProvider *_dateProvider; id _sentryRandom; @@ -48,7 +48,7 @@ @implementation SentrySessionReplay { - (instancetype)initWithSettings:(SentryReplayOptions *)replayOptions replayFolderPath:(NSURL *)folderPath screenshotProvider:(id)screenshotProvider - replayMaker:(id)replayMaker + replayMaker:(id)replayMaker dateProvider:(SentryCurrentDateProvider *)dateProvider random:(id)random displayLinkWrapper:(SentryDisplayLinkWrapper *)displayLinkWrapper; @@ -242,6 +242,7 @@ - (void)createAndCapture:(NSURL *)videoUrl duration:(NSTimeInterval)duration startedAt:(NSDate *)start { + __weak SentrySessionReplay *weakSelf = self; [_replayMaker createVideoWithDuration:duration beginning:start @@ -251,17 +252,22 @@ - (void)createAndCapture:(NSURL *)videoUrl if (error != nil) { SENTRY_LOG_ERROR(@"Could not create replay video - %@", error); } else { - [self captureSegment:self->_currentSegmentId++ - video:videoInfo - replayId:self->_sessionReplayId - replayType:kSentryReplayTypeSession]; - - [self->_replayMaker releaseFramesUntil:videoInfo.end]; - self->_videoSegmentStart = nil; + [weakSelf newSegmentAvailable:videoInfo]; } }]; } +- (void)newSegmentAvailable:(SentryVideoInfo *)videoInfo +{ + [self captureSegment:self->_currentSegmentId++ + video:videoInfo + replayId:self->_sessionReplayId + replayType:kSentryReplayTypeSession]; + + [_replayMaker releaseFramesUntil:videoInfo.end]; + _videoSegmentStart = nil; +} + - (void)captureSegment:(NSInteger)segment video:(SentryVideoInfo *)videoInfo replayId:(SentryId *)replayid @@ -306,11 +312,16 @@ - (void)takeScreenshot _processingScreenshot = YES; } - UIImage *screenshot = [_screenshotProvider imageWithView:_rootView options:_replayOptions]; + __weak SentrySessionReplay *weakSelf = self; + [_screenshotProvider imageWithView:_rootView + options:_replayOptions + onComplete:^(UIImage *screenshot) { [weakSelf newImage:screenshot]; }]; +} +- (void)newImage:(UIImage *)image +{ _processingScreenshot = NO; - - [self->_replayMaker addFrameAsyncWithImage:screenshot]; + [_replayMaker addFrameAsyncWithImage:image]; } @end diff --git a/Sources/Sentry/SentrySessionReplayIntegration.m b/Sources/Sentry/SentrySessionReplayIntegration.m index 7007b66c73c..df7a7dd56f2 100644 --- a/Sources/Sentry/SentrySessionReplayIntegration.m +++ b/Sources/Sentry/SentrySessionReplayIntegration.m @@ -28,17 +28,6 @@ - (void)newSceneActivate; @end -API_AVAILABLE(ios(16.0), tvos(16.0)) -@interface -SentryViewPhotographer (SentryViewScreenshotProvider) -@end - -API_AVAILABLE(ios(16.0), tvos(16.0)) -@interface -SentryOnDemandReplay (SentryReplayMaker) - -@end - @implementation SentrySessionReplayIntegration { BOOL _startedAsFullSession; SentryReplayOptions *_replayOptions; diff --git a/Sources/Sentry/include/SentryCoreGraphicsHelper.h b/Sources/Sentry/include/SentryCoreGraphicsHelper.h deleted file mode 100644 index e561984de1b..00000000000 --- a/Sources/Sentry/include/SentryCoreGraphicsHelper.h +++ /dev/null @@ -1,13 +0,0 @@ -#import "SentryDefines.h" -#import -#import - -NS_ASSUME_NONNULL_BEGIN -#if SENTRY_HAS_UIKIT - -@interface SentryCoreGraphicsHelper : NSObject -+ (CGMutablePathRef)excludeRect:(CGRect)rectangle fromPath:(CGMutablePathRef)path; -@end - -#endif -NS_ASSUME_NONNULL_END diff --git a/Sources/Sentry/include/SentryPrivate.h b/Sources/Sentry/include/SentryPrivate.h index 5d66a9971e1..c2507583c5a 100644 --- a/Sources/Sentry/include/SentryPrivate.h +++ b/Sources/Sentry/include/SentryPrivate.h @@ -11,4 +11,3 @@ // Headers that also import SentryDefines should be at the end of this list // otherwise it wont compile -#import "SentryCoreGraphicsHelper.h" diff --git a/Sources/Sentry/include/SentrySessionReplay.h b/Sources/Sentry/include/SentrySessionReplay.h index 1d8293ccaad..123f1252325 100644 --- a/Sources/Sentry/include/SentrySessionReplay.h +++ b/Sources/Sentry/include/SentrySessionReplay.h @@ -13,27 +13,11 @@ @protocol SentryRandom; @protocol SentryRedactOptions; +@protocol SentryViewScreenshotProvider; +@protocol SentryReplayVideoMaker; NS_ASSUME_NONNULL_BEGIN -@protocol SentryReplayMaker - -- (void)addFrameAsyncWithImage:(UIImage *)image; -- (void)releaseFramesUntil:(NSDate *)date; -- (BOOL)createVideoWithDuration:(NSTimeInterval)duration - beginning:(NSDate *)beginning - outputFileURL:(NSURL *)outputFileURL - error:(NSError *_Nullable *_Nullable)error - completion: - (void (^)(SentryVideoInfo *_Nullable, NSError *_Nullable))completion; - -@end - -@protocol SentryViewScreenshotProvider -- (UIImage *)imageWithView:(UIView *)view options:(id)options; -@end - -API_AVAILABLE(ios(16.0), tvos(16.0)) @interface SentrySessionReplay : NSObject @property (nonatomic, strong, readonly) SentryId *sessionReplayId; @@ -41,7 +25,7 @@ API_AVAILABLE(ios(16.0), tvos(16.0)) - (instancetype)initWithSettings:(SentryReplayOptions *)replayOptions replayFolderPath:(NSURL *)folderPath screenshotProvider:(id)photographer - replayMaker:(id)replayMaker + replayMaker:(id)replayMaker dateProvider:(SentryCurrentDateProvider *)dateProvider random:(id)random displayLinkWrapper:(SentryDisplayLinkWrapper *)displayLinkWrapper; diff --git a/Sources/Swift/Integrations/SessionReplay/SentryOnDemandReplay.swift b/Sources/Swift/Integrations/SessionReplay/SentryOnDemandReplay.swift index dd343b8e50b..4b299e46fa5 100644 --- a/Sources/Swift/Integrations/SessionReplay/SentryOnDemandReplay.swift +++ b/Sources/Swift/Integrations/SessionReplay/SentryOnDemandReplay.swift @@ -22,7 +22,7 @@ enum SentryOnDemandReplayError: Error { } @objcMembers -class SentryOnDemandReplay: NSObject { +class SentryOnDemandReplay: NSObject, SentryReplayVideoMaker { private let _outputPath: String private var _currentPixelBuffer: SentryPixelBuffer? private var _totalFrames = 0 diff --git a/Sources/Swift/Integrations/SessionReplay/SentryReplayVideoMaker.swift b/Sources/Swift/Integrations/SessionReplay/SentryReplayVideoMaker.swift new file mode 100644 index 00000000000..e10ca6bf3ef --- /dev/null +++ b/Sources/Swift/Integrations/SessionReplay/SentryReplayVideoMaker.swift @@ -0,0 +1,11 @@ +#if canImport(UIKit) +import Foundation +import UIKit + +@objc +protocol SentryReplayVideoMaker: NSObjectProtocol { + func addFrameAsync(image: UIImage) + func releaseFramesUntil(_ date: Date) + func createVideoWith(duration: TimeInterval, beginning: Date, outputFileURL: URL, completion: @escaping (SentryVideoInfo?, Error?) -> Void) throws +} +#endif diff --git a/Sources/Swift/Tools/SentryViewPhotographer.swift b/Sources/Swift/Tools/SentryViewPhotographer.swift index 3ae3f54d7de..d1089a801f2 100644 --- a/Sources/Swift/Tools/SentryViewPhotographer.swift +++ b/Sources/Swift/Tools/SentryViewPhotographer.swift @@ -1,123 +1,47 @@ #if canImport(UIKit) && !SENTRY_NO_UIKIT #if os(iOS) || os(tvOS) -@_implementationOnly import _SentryPrivate import CoreGraphics import Foundation import UIKit -@available(iOS, introduced: 16.0) -@available(tvOS, introduced: 16.0) @objcMembers -class SentryViewPhotographer: NSObject { - - //This is a list of UIView subclasses that will be ignored during redact process - private var ignoreClasses: [AnyClass] = [] - //This is a list of UIView subclasses that need to be redacted from screenshot - private var redactClasses: [AnyClass] = [] +class SentryViewPhotographer: NSObject, SentryViewScreenshotProvider { static let shared = SentryViewPhotographer() - override init() { -#if os(iOS) - ignoreClasses = [ UISlider.self, UISwitch.self ] -#endif // os(iOS) - redactClasses = [ UILabel.self, UITextView.self, UITextField.self ] + [ - "_TtCOCV7SwiftUI11DisplayList11ViewUpdater8Platform13CGDrawingView", - "_TtC7SwiftUIP33_A34643117F00277B93DEBAB70EC0697122_UIShapeHitTestingView", - "SwiftUI._UIGraphicsView", "SwiftUI.ImageLayer" - ].compactMap { NSClassFromString($0) } - } - - @objc(imageWithView:options:) - func image(view: UIView, options: SentryRedactOptions) -> UIImage? { - UIGraphicsBeginImageContextWithOptions(view.bounds.size, true, 0) + //This is a list of UIView subclasses that will be ignored during redact process + private var redactBuilder = UIRedactBuilder() - defer { - UIGraphicsEndImageContext() + func image(view: UIView, options: SentryRedactOptions, onComplete: @escaping ScreenshotCallback ) { + let image = UIGraphicsImageRenderer(size: view.bounds.size).image { _ in + view.drawHierarchy(in: view.bounds, afterScreenUpdates: false) } - guard let currentContext = UIGraphicsGetCurrentContext() else { return nil } - - view.layer.render(in: currentContext) - self.mask(view: view, context: currentContext, options: options) - - guard let screenshot = UIGraphicsGetImageFromCurrentImageContext() else { return nil } - return screenshot + let redact = redactBuilder.redactRegionsFor(view: view, options: options) + let imageSize = view.bounds.size + DispatchQueue.global().async { + let screenshot = UIGraphicsImageRenderer(size: imageSize, format: .init(for: .init(displayScale: 1))).image { context in + context.cgContext.interpolationQuality = .none + image.draw(at: .zero) + + for region in redact { + (region.color ?? UIImageHelper.averageColor(of: context.currentImage, at: region.rect)).setFill() + context.fill(region.rect) + } + } + onComplete(screenshot) + } } - + @objc(addIgnoreClasses:) func addIgnoreClasses(classes: [AnyClass]) { - ignoreClasses += classes + redactBuilder.ignoreClasses += classes } @objc(addRedactClasses:) func addRedactClasses(classes: [AnyClass]) { - redactClasses += classes - } - - private func mask(view: UIView, context: CGContext, options: SentryRedactOptions?) { - UIColor.black.setFill() - let maskPath = self.buildPath(view: view, - path: CGMutablePath(), - area: view.frame, - redactText: options?.redactAllText ?? true, - redactImage: options?.redactAllImages ?? true) - context.addPath(maskPath) - context.fillPath() - } - - private func shouldIgnore(view: UIView) -> Bool { - ignoreClasses.contains { view.isKind(of: $0) } - } - - private func shouldRedact(view: UIView) -> Bool { - return redactClasses.contains { view.isKind(of: $0) } - } - - private func shouldRedact(imageView: UIImageView) -> Bool { - // Checking the size is to avoid redact gradient backgroud that - // are usually small lines repeating - guard let image = imageView.image, image.size.width > 10 && image.size.height > 10 else { return false } - return image.imageAsset?.value(forKey: "_containingBundle") == nil - } - - private func buildPath(view: UIView, path: CGMutablePath, area: CGRect, redactText: Bool, redactImage: Bool) -> CGMutablePath { - let rectInWindow = view.convert(view.bounds, to: nil) - - if (!redactImage && !redactText) || !area.intersects(rectInWindow) || view.isHidden || view.alpha == 0 { - return path - } - - var result = path - - let ignore = shouldIgnore(view: view) - - let redact: Bool = { - if redactImage, let imageView = view as? UIImageView { - return shouldRedact(imageView: imageView) - } - return redactText && shouldRedact(view: view) - }() - - if !ignore && redact { - result.addRect(rectInWindow) - return result - } else if isOpaqueOrHasBackground(view) { - result = SentryCoreGraphicsHelper.excludeRect(rectInWindow, from: result).takeRetainedValue() - } - - if !ignore { - for subview in view.subviews { - result = buildPath(view: subview, path: path, area: area, redactText: redactText, redactImage: redactImage) - } - } - - return result - } - - private func isOpaqueOrHasBackground(_ view: UIView) -> Bool { - return view.isOpaque || (view.backgroundColor != nil && (view.backgroundColor?.cgColor.alpha ?? 0) > 0.9) + redactBuilder.redactClasses += classes } } diff --git a/Sources/Swift/Tools/SentryViewScreenshotProvider.swift b/Sources/Swift/Tools/SentryViewScreenshotProvider.swift new file mode 100644 index 00000000000..7fc012deeb5 --- /dev/null +++ b/Sources/Swift/Tools/SentryViewScreenshotProvider.swift @@ -0,0 +1,13 @@ +#if canImport(UIKit) && !SENTRY_NO_UIKIT +#if os(iOS) || os(tvOS) +import Foundation +import UIKit + +typealias ScreenshotCallback = (UIImage) -> Void + +@objc +protocol SentryViewScreenshotProvider: NSObjectProtocol { + func image(view: UIView, options: SentryRedactOptions, onComplete: @escaping ScreenshotCallback) +} +#endif +#endif diff --git a/Sources/Swift/Tools/UIImageHelper.swift b/Sources/Swift/Tools/UIImageHelper.swift new file mode 100644 index 00000000000..f53a95995aa --- /dev/null +++ b/Sources/Swift/Tools/UIImageHelper.swift @@ -0,0 +1,36 @@ +#if canImport(UIKit) && !SENTRY_NO_UIKIT +#if os(iOS) || os(tvOS) + +import Foundation +import UIKit + +final class UIImageHelper { + private init() { } + + static func averageColor(of image: UIImage, at region: CGRect) -> UIColor { + let scaledRegion = region.applying(CGAffineTransform(scaleX: image.scale, y: image.scale)) + guard let croppedImage = image.cgImage?.cropping(to: scaledRegion), let colorSpace = croppedImage.colorSpace else { + return .black + } + + let bitmapInfo: UInt32 = CGBitmapInfo.byteOrder32Little.rawValue | CGImageAlphaInfo.premultipliedFirst.rawValue + + guard let context = CGContext(data: nil, width: 1, height: 1, bitsPerComponent: 8, bytesPerRow: 4, space: colorSpace, bitmapInfo: bitmapInfo) else { return .black } + context.interpolationQuality = .high + context.draw(croppedImage, in: CGRect(x: 0, y: 0, width: 1, height: 1)) + guard let pixelBuffer = context.data else { return .black } + + let data = pixelBuffer.bindMemory(to: UInt8.self, capacity: 4) + + let blue = CGFloat(data[0]) / 255.0 + let green = CGFloat(data[1]) / 255.0 + let red = CGFloat(data[2]) / 255.0 + let alpha = CGFloat(data[3]) / 255.0 + + return UIColor(red: red, green: green, blue: blue, alpha: alpha) + } + +} + +#endif +#endif diff --git a/Sources/Swift/Tools/UIRedactBuilder.swift b/Sources/Swift/Tools/UIRedactBuilder.swift new file mode 100644 index 00000000000..bb9c5a4b4eb --- /dev/null +++ b/Sources/Swift/Tools/UIRedactBuilder.swift @@ -0,0 +1,139 @@ +#if canImport(UIKit) && !SENTRY_NO_UIKIT +#if os(iOS) || os(tvOS) +import Foundation +import UIKit + +struct RedactRegion { + let rect: CGRect + let color: UIColor? + + init(rect: CGRect, color: UIColor? = nil) { + self.rect = rect + self.color = color + } + + func splitBySubtracting(region: CGRect) -> [RedactRegion] { + guard rect.intersects(region) else { return [self] } + guard !region.contains(rect) else { return [] } + + let intersectionRect = rect.intersection(region) + var resultRegions: [CGRect] = [] + + // Calculate the top region. + resultRegions.append(CGRect(x: rect.minX, + y: rect.minY, + width: rect.width, + height: intersectionRect.minY - rect.minY)) + + // Calculate the bottom region. + resultRegions.append(CGRect(x: rect.minX, + y: intersectionRect.maxY, + width: rect.width, + height: rect.maxY - intersectionRect.maxY)) + + // Calculate the left region. + resultRegions.append(CGRect(x: rect.minX, + y: max(rect.minY, intersectionRect.minY), + width: intersectionRect.minX - rect.minX, + height: min(intersectionRect.maxY, rect.maxY) - max(rect.minY, intersectionRect.minY))) + + // Calculate the right region. + resultRegions.append(CGRect(x: intersectionRect.maxX, + y: max(rect.minY, intersectionRect.minY), + width: rect.maxX - intersectionRect.maxX, + height: min(intersectionRect.maxY, rect.maxY) - max(rect.minY, intersectionRect.minY))) + + return resultRegions.filter { !$0.isEmpty }.map { RedactRegion(rect: $0, color: color) } + } +} + +class UIRedactBuilder { + + //This is a list of UIView subclasses that will be ignored during redact process + var ignoreClasses: [AnyClass] + //This is a list of UIView subclasses that need to be redacted from screenshot + var redactClasses: [AnyClass] + + init() { + + redactClasses = [ UILabel.self, UITextView.self, UITextField.self ] + + //this classes are used by SwiftUI to display images. + ["_TtCOCV7SwiftUI11DisplayList11ViewUpdater8Platform13CGDrawingView", + "_TtC7SwiftUIP33_A34643117F00277B93DEBAB70EC0697122_UIShapeHitTestingView", + "SwiftUI._UIGraphicsView", "SwiftUI.ImageLayer" + ].compactMap { NSClassFromString($0) } +#if os(iOS) + ignoreClasses = [ UISlider.self, UISwitch.self ] +#else + ignoreClasses = [] +#endif + } + + func redactRegionsFor(view: UIView, options: SentryRedactOptions?) -> [RedactRegion] { + var redactingRegions = [RedactRegion]() + + self.mapRedactRegion(fromView: view, + to: view, + redacting: &redactingRegions, + area: view.frame, + redactText: options?.redactAllText ?? true, + redactImage: options?.redactAllImages ?? true) + + return redactingRegions + } + + private func shouldIgnore(view: UIView) -> Bool { + ignoreClasses.contains { view.isKind(of: $0) } + } + + private func shouldRedact(view: UIView, redactText: Bool, redactImage: Bool) -> Bool { + if redactImage, let imageView = view as? UIImageView { + return shouldRedact(imageView: imageView) + } + return redactText && redactClasses.contains { view.isKind(of: $0) } + } + + private func shouldRedact(imageView: UIImageView) -> Bool { + // Checking the size is to avoid redact gradient background that + // are usually small lines repeating + guard let image = imageView.image, image.size.width > 10 && image.size.height > 10 else { return false } + return image.imageAsset?.value(forKey: "_containingBundle") == nil + } + + private func mapRedactRegion(fromView view: UIView, to: UIView, redacting: inout [RedactRegion], area: CGRect, redactText: Bool, redactImage: Bool) { + let rectInWindow = view.convert(view.bounds, to: to) + guard (redactImage || redactText) && area.intersects(rectInWindow) && !view.isHidden && view.alpha != 0 else { return } + + let ignore = shouldIgnore(view: view) + let redact = shouldRedact(view: view, redactText: redactText, redactImage: redactImage) + + if !ignore && redact { + redacting.append(RedactRegion(rect: rectInWindow, color: self.color(for: view))) + return + } else if hasBackground(view) { + if rectInWindow == area { + redacting.removeAll() + } else { + redacting = redacting.flatMap { $0.splitBySubtracting(region: rectInWindow) } + } + } + + if !ignore { + for subview in view.subviews { + mapRedactRegion(fromView: subview, to: to, redacting: &redacting, area: area, redactText: redactText, redactImage: redactImage) + } + } + } + + private func color(for view: UIView) -> UIColor? { + return (view as? UILabel)?.textColor + } + + private func hasBackground(_ view: UIView) -> Bool { + //Anything with an alpha greater than 0.9 is opaque enough that it's impossible to see anything behind it. + return view.backgroundColor != nil && (view.backgroundColor?.cgColor.alpha ?? 0) > 0.9 + } +} + +#endif +#endif diff --git a/Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayTests.swift b/Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayTests.swift index 03d00728f2d..a21a260ca7b 100644 --- a/Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayTests.swift +++ b/Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayTests.swift @@ -8,10 +8,13 @@ import XCTest class SentrySessionReplayTests: XCTestCase { private class ScreenshotProvider: NSObject, SentryViewScreenshotProvider { - func image(with view: UIView, options: SentryRedactOptions) -> UIImage { UIImage.add } + func image(view: UIView, options: Sentry.SentryRedactOptions, onComplete: @escaping Sentry.ScreenshotCallback) { + onComplete(UIImage.add) + } } - private class TestReplayMaker: NSObject, SentryReplayMaker { + private class TestReplayMaker: NSObject, SentryReplayVideoMaker { + struct CreateVideoCall { var duration: TimeInterval var beginning: Date @@ -20,8 +23,8 @@ class SentrySessionReplayTests: XCTestCase { } var lastCallToCreateVideo: CreateVideoCall? - func createVideo(withDuration duration: TimeInterval, beginning: Date, outputFileURL: URL, completion: @escaping (SentryVideoInfo?, Error?) -> Void) throws { - lastCallToCreateVideo = CreateVideoCall(duration: duration, + func createVideoWith(duration: TimeInterval, beginning: Date, outputFileURL: URL, completion: @escaping (Sentry.SentryVideoInfo?, (Error)?) -> Void) throws { + lastCallToCreateVideo = CreateVideoCall(duration: duration, beginning: beginning, outputFileURL: outputFileURL, completion: completion) @@ -34,12 +37,12 @@ class SentrySessionReplayTests: XCTestCase { } var lastFrame: UIImage? - func addFrameAsync(with image: UIImage) { + func addFrameAsync(image: UIImage) { lastFrame = image } var lastReleaseUntil: Date? - func releaseFrames(until date: Date) { + func releaseFramesUntil(_ date: Date) { lastReleaseUntil = date } } @@ -56,7 +59,6 @@ class SentrySessionReplayTests: XCTestCase { } } - @available(iOS 16.0, tvOS 16.0, *) private class Fixture { let dateProvider = TestCurrentDateProvider() let random = TestRandom(value: 0) @@ -71,19 +73,13 @@ class SentrySessionReplayTests: XCTestCase { return SentrySessionReplay(settings: options, replayFolderPath: cacheFolder, screenshotProvider: screenshotProvider, - replayMaker: replayMaker, + replay: replayMaker, dateProvider: dateProvider, random: random, displayLinkWrapper: displayLink) } } - override func setUpWithError() throws { - guard #available(iOS 16.0, tvOS 16.0, *) else { - throw XCTSkip("iOS version not supported") - } - } - override func setUp() { super.setUp() } @@ -93,14 +89,12 @@ class SentrySessionReplayTests: XCTestCase { clearTestState() } - @available(iOS 16.0, tvOS 16, *) private func startFixture() -> Fixture { let fixture = Fixture() SentrySDK.setCurrentHub(fixture.hub) return fixture } - @available(iOS 16.0, tvOS 16, *) func testDontSentReplay_NoFullSession() { let fixture = startFixture() let sut = fixture.getSut() @@ -114,7 +108,6 @@ class SentrySessionReplayTests: XCTestCase { expect(fixture.hub.lastEvent) == nil } - @available(iOS 16.0, tvOS 16, *) func testSentReplay_FullSession() { let fixture = startFixture() @@ -144,7 +137,6 @@ class SentrySessionReplayTests: XCTestCase { assertFullSession(sut, expected: true) } - @available(iOS 16.0, tvOS 16, *) func testDontSentReplay_NotFullSession() { let fixture = startFixture() let sut = fixture.getSut(options: SentryReplayOptions(sessionSampleRate: 1, errorSampleRate: 1)) @@ -164,7 +156,6 @@ class SentrySessionReplayTests: XCTestCase { assertFullSession(sut, expected: false) } - @available(iOS 16.0, tvOS 16, *) func testChangeReplayMode_forErrorEvent() { let fixture = startFixture() let sut = fixture.getSut(options: SentryReplayOptions(sessionSampleRate: 1, errorSampleRate: 1)) @@ -178,7 +169,6 @@ class SentrySessionReplayTests: XCTestCase { assertFullSession(sut, expected: true) } - @available(iOS 16.0, tvOS 16, *) func testDontChangeReplayMode_forNonErrorEvent() { let fixture = startFixture() let sut = fixture.getSut(options: SentryReplayOptions(sessionSampleRate: 1, errorSampleRate: 1)) @@ -219,7 +209,6 @@ class SentrySessionReplayTests: XCTestCase { expect(Dynamic(sut).isRunning) == false } - @available(iOS 16.0, tvOS 16, *) func assertFullSession(_ sessionReplay: SentrySessionReplay, expected: Bool) { expect(Dynamic(sessionReplay).isFullSession) == expected } diff --git a/Tests/SentryTests/RedactRegionTests.swift b/Tests/SentryTests/RedactRegionTests.swift new file mode 100644 index 00000000000..31919e18008 --- /dev/null +++ b/Tests/SentryTests/RedactRegionTests.swift @@ -0,0 +1,143 @@ +import Foundation +import Nimble +@testable import Sentry +import XCTest +#if os(iOS) || os(tvOS) +class RedactRegionTests: XCTestCase { + + func testSplitBySubtractingBottom() { + let sut = RedactRegion(rect: CGRect(x: 0, y: 0, width: 100, height: 100), color: .red) + + let result = sut.splitBySubtracting(region: CGRect(x: 0, y: 50, width: 100, height: 50)) + + expect(result.count) == 1 + expect(result.first?.rect) == CGRect(x: 0, y: 0, width: 100, height: 50) + expect(result.first?.color) == .red + } + + func testSplitBySubtractingTop() { + let sut = RedactRegion(rect: CGRect(x: 0, y: 0, width: 100, height: 100)) + + let result = sut.splitBySubtracting(region: CGRect(x: 0, y: 0, width: 100, height: 50)) + + expect(result.count) == 1 + expect(result.first?.rect) == CGRect(x: 0, y: 50, width: 100, height: 50) + } + + func testSplitBySubtractingTopRight() { + let sut = RedactRegion(rect: CGRect(x: 0, y: 0, width: 100, height: 100)) + + let result = sut.splitBySubtracting(region: CGRect(x: 50, y: 0, width: 50, height: 50)) + + expect(result.count) == 2 + expect(result.first?.rect) == CGRect(x: 0, y: 50, width: 100, height: 50) + expect(result[1].rect) == CGRect(x: 0, y: 0, width: 50, height: 50) + } + + func testSplitBySubtractingBottomLeft() { + let sut = RedactRegion(rect: CGRect(x: 0, y: 0, width: 100, height: 100)) + + let result = sut.splitBySubtracting(region: CGRect(x: 0, y: 50, width: 50, height: 50)) + + expect(result.count) == 2 + expect(result.first?.rect) == CGRect(x: 0, y: 0, width: 100, height: 50) + expect(result[1].rect) == CGRect(x: 50, y: 50, width: 50, height: 50) + } + + func testSplitBySubtractingMiddle() { + let sut = RedactRegion(rect: CGRect(x: 0, y: 0, width: 100, height: 100)) + + let result = sut.splitBySubtracting(region: CGRect(x: 25, y: 25, width: 50, height: 50)) + + expect(result.count) == 4 + expect(result[0].rect) == CGRect(x: 0, y: 0, width: 100, height: 25) + expect(result[1].rect) == CGRect(x: 0, y: 75, width: 100, height: 25) + expect(result[2].rect) == CGRect(x: 0, y: 25, width: 25, height: 50) + expect(result[3].rect) == CGRect(x: 75, y: 25, width: 25, height: 50) + } + + func testSplitBySubtractingInHalfHorizontally() { + let sut = RedactRegion(rect: CGRect(x: 0, y: 0, width: 100, height: 100)) + + let result = sut.splitBySubtracting(region: CGRect(x: 0, y: 25, width: 100, height: 50)) + + expect(result.count) == 2 + expect(result[0].rect) == CGRect(x: 0, y: 0, width: 100, height: 25) + expect(result[1].rect) == CGRect(x: 0, y: 75, width: 100, height: 25) + } + + func testSplitBySubtractingInHalfVertically() { + let sut = RedactRegion(rect: CGRect(x: 0, y: 0, width: 100, height: 100)) + + let result = sut.splitBySubtracting(region: CGRect(x: 25, y: 0, width: 50, height: 100)) + + expect(result.count) == 2 + expect(result[0].rect) == CGRect(x: 0, y: 0, width: 25, height: 100) + expect(result[1].rect) == CGRect(x: 75, y: 0, width: 25, height: 100) + } + + func testSplitBySubtractingMiddleRight() { + let sut = RedactRegion(rect: CGRect(x: 0, y: 0, width: 100, height: 100)) + + let result = sut.splitBySubtracting(region: CGRect(x: 25, y: 25, width: 100, height: 50)) + + expect(result.count) == 3 + expect(result[0].rect) == CGRect(x: 0, y: 0, width: 100, height: 25) + expect(result[1].rect) == CGRect(x: 0, y: 75, width: 100, height: 25) + expect(result[2].rect) == CGRect(x: 0, y: 25, width: 25, height: 50) + } + + func testSplitBySubtractingMiddleLeft() { + let sut = RedactRegion(rect: CGRect(x: 50, y: 0, width: 100, height: 100)) + + let result = sut.splitBySubtracting(region: CGRect(x: 0, y: 25, width: 100, height: 50)) + + expect(result.count) == 3 + expect(result[0].rect) == CGRect(x: 50, y: 0, width: 100, height: 25) + expect(result[1].rect) == CGRect(x: 50, y: 75, width: 100, height: 25) + expect(result[2].rect) == CGRect(x: 100, y: 25, width: 50, height: 50) + } + + func testSplitBySubtracting_TopIsWider() { + let sut = RedactRegion(rect: CGRect(x: 0, y: 0, width: 100, height: 100), color: .red) + let result = sut.splitBySubtracting(region: CGRect(x: 0, y: 0, width: 150, height: 50)) + + expect(result.count) == 1 + expect(result.first?.rect) == CGRect(x: 0, y: 50, width: 100, height: 50) + expect(result.first?.color) == .red + } + + func testSplitBySubtracting_BottomIsWider() { + let sut = RedactRegion(rect: CGRect(x: 0, y: 0, width: 100, height: 100), color: .red) + let result = sut.splitBySubtracting(region: CGRect(x: 0, y: 50, width: 150, height: 50)) + + expect(result.count) == 1 + expect(result.first?.rect) == CGRect(x: 0, y: 0, width: 100, height: 50) + expect(result.first?.color) == .red + } + + func testNoResultForEqualRegion() { + let sut = RedactRegion(rect: CGRect(x: 0, y: 0, width: 100, height: 100), color: .red) + let result = sut.splitBySubtracting(region: CGRect(x: 0, y: 0, width: 100, height: 100)) + + expect(result.count) == 0 + } + + func testNoResultForLargerRegion() { + let sut = RedactRegion(rect: CGRect(x: 50, y: 50, width: 100, height: 100), color: .red) + let result = sut.splitBySubtracting(region: CGRect(x: 0, y: 0, width: 200, height: 200)) + + expect(result.count) == 0 + } + + func testSameRegionForOutsideOfBounds() { + let sut = RedactRegion(rect: CGRect(x: 0, y: 0, width: 100, height: 100), color: .red) + let result = sut.splitBySubtracting(region: CGRect(x: 110, y: 110, width: 200, height: 200)) + + expect(result.count) == 1 + expect(result.first?.rect) == sut.rect + expect(result.first?.color) == .red + } + +} +#endif diff --git a/Tests/SentryTests/UIImageHelperTests.swift b/Tests/SentryTests/UIImageHelperTests.swift new file mode 100644 index 00000000000..608d98df436 --- /dev/null +++ b/Tests/SentryTests/UIImageHelperTests.swift @@ -0,0 +1,65 @@ +#if canImport(UIKit) +import Foundation +import Nimble +@testable import Sentry +import XCTest + +class UIImageHelperTests: XCTestCase { + + private let testFrame = CGRect(x: 0, y: 0, width: 100, height: 100) + + func testAverageColorRed() { + let begin = Date() + let image = UIGraphicsImageRenderer(size: testFrame.size).image { context in + UIColor.red.setFill() + context.fill(testFrame) + } + + expect(UIImageHelper.averageColor(of: image, at: self.testFrame)) == .red + + let end = Date() + print("Duration = \(end.timeIntervalSince(begin))") + } + + func testAverageColorGreen() { + let image = UIGraphicsImageRenderer(size: testFrame.size).image { context in + UIColor.green.setFill() + context.fill(testFrame) + } + + expect(UIImageHelper.averageColor(of: image, at: self.testFrame)) == .green + } + + func testAverageColorBlue() { + let image = UIGraphicsImageRenderer(size: testFrame.size).image { context in + UIColor.blue.setFill() + context.fill(testFrame) + } + + expect(UIImageHelper.averageColor(of: image, at: self.testFrame)) == .blue + } + + func testAverageColorYellow() { + let image = UIGraphicsImageRenderer(size: testFrame.size).image { context in + UIColor.yellow.setFill() + context.fill(testFrame) + } + + expect(UIImageHelper.averageColor(of: image, at: self.testFrame)) == .yellow + } + + func testGreenAreaInARedImage() { + let focusArea = CGRect(x: 25, y: 25, width: 50, height: 50) + + let image = UIGraphicsImageRenderer(size: testFrame.size).image { context in + UIColor.red.setFill() + context.fill(testFrame) + UIColor.green.setFill() + context.fill(focusArea) + } + + expect(UIImageHelper.averageColor(of: image, at: focusArea)) == .green + } +} + +#endif diff --git a/Tests/SentryTests/UIRedactBuilderTests.swift b/Tests/SentryTests/UIRedactBuilderTests.swift new file mode 100644 index 00000000000..f6adf66a20d --- /dev/null +++ b/Tests/SentryTests/UIRedactBuilderTests.swift @@ -0,0 +1,162 @@ +#if os(iOS) +import Foundation +import Nimble +@testable import Sentry +import UIKit +import XCTest + +class UIRedactBuilderTests: XCTestCase { + + private class RedactOptions: SentryRedactOptions { + var redactAllText: Bool + var redactAllImages: Bool + + init(redactAllText: Bool = true, redactAllImages: Bool = true) { + self.redactAllText = redactAllText + self.redactAllImages = redactAllImages + } + } + + private let rootView = UIView(frame: CGRect(x: 0, y: 0, width: 100, height: 100)) + + func testNoNeedForRedact() { + let sut = UIRedactBuilder() + rootView.addSubview(UIView(frame: CGRect(x: 20, y: 20, width: 40, height: 40))) + + let result = sut.redactRegionsFor(view: rootView, options: RedactOptions()) + + expect(result.count) == 0 + } + + func testRedactALabel() { + let sut = UIRedactBuilder() + let label = UILabel(frame: CGRect(x: 20, y: 20, width: 40, height: 40)) + label.textColor = .purple + rootView.addSubview(label) + + let result = sut.redactRegionsFor(view: rootView, options: RedactOptions()) + + expect(result.count) == 1 + expect(result.first?.color) == .purple + expect(result.first?.rect) == CGRect(x: 20, y: 20, width: 40, height: 40) + } + + func testDontRedactALabelOptionDisabled() { + let sut = UIRedactBuilder() + let label = UILabel(frame: CGRect(x: 20, y: 20, width: 40, height: 40)) + label.textColor = .purple + rootView.addSubview(label) + + let result = sut.redactRegionsFor(view: rootView, options: RedactOptions(redactAllText: false)) + + expect(result.count) == 0 + } + + func testRedactAImage() { + let sut = UIRedactBuilder() + + let image = UIGraphicsImageRenderer(size: CGSize(width: 40, height: 40)).image { context in + context.fill(CGRect(x: 0, y: 0, width: 40, height: 40)) + } + + let imageView = UIImageView(image: image) + imageView.frame = CGRect(x: 20, y: 20, width: 40, height: 40) + rootView.addSubview(imageView) + + let result = sut.redactRegionsFor(view: rootView, options: RedactOptions()) + + expect(result.count) == 1 + expect(result.first?.color) == nil + expect(result.first?.rect) == CGRect(x: 20, y: 20, width: 40, height: 40) + } + + func testDontRedactAImageOptionDisabled() { + let sut = UIRedactBuilder() + + let image = UIGraphicsImageRenderer(size: CGSize(width: 40, height: 40)).image { context in + context.fill(CGRect(x: 0, y: 0, width: 40, height: 40)) + } + + let imageView = UIImageView(image: image) + imageView.frame = CGRect(x: 20, y: 20, width: 40, height: 40) + rootView.addSubview(imageView) + + let result = sut.redactRegionsFor(view: rootView, options: RedactOptions(redactAllImages: false)) + + expect(result.count) == 0 + } + + func testDontRedactABundleImage() { + //The check for bundled image only works for iOS 16 and above + //For others versions all images will be redacted + guard #available(iOS 16, *) else { return } + let sut = UIRedactBuilder() + + let imageView = UIImageView(image: .add) + imageView.frame = CGRect(x: 20, y: 20, width: 40, height: 40) + rootView.addSubview(imageView) + + let result = sut.redactRegionsFor(view: rootView, options: RedactOptions()) + + expect(result.count) == 0 + } + + func testDontRedactAHiddenView() { + let sut = UIRedactBuilder() + let label = UILabel(frame: CGRect(x: 20, y: 20, width: 40, height: 40)) + label.isHidden = true + rootView.addSubview(label) + + let result = sut.redactRegionsFor(view: rootView, options: RedactOptions()) + + expect(result.count) == 0 + } + + func testDontRedactATransparentView() { + let sut = UIRedactBuilder() + let label = UILabel(frame: CGRect(x: 20, y: 20, width: 40, height: 40)) + label.alpha = 0 + rootView.addSubview(label) + + let result = sut.redactRegionsFor(view: rootView, options: RedactOptions()) + + expect(result.count) == 0 + } + + func testDontRedactALabelBehindAOpaqueView() { + let sut = UIRedactBuilder() + let label = UILabel(frame: CGRect(x: 20, y: 20, width: 40, height: 40)) + rootView.addSubview(label) + let topView = UIView(frame: CGRect(x: 10, y: 10, width: 60, height: 60)) + topView.backgroundColor = .white + rootView.addSubview(topView) + let result = sut.redactRegionsFor(view: rootView, options: RedactOptions()) + expect(result.count) == 0 + } + + func testRedactALabelBehindATransparentView() { + let sut = UIRedactBuilder() + let label = UILabel(frame: CGRect(x: 20, y: 20, width: 40, height: 40)) + rootView.addSubview(label) + let topView = UIView(frame: CGRect(x: 10, y: 10, width: 60, height: 60)) + topView.backgroundColor = .clear + rootView.addSubview(topView) + let result = sut.redactRegionsFor(view: rootView, options: RedactOptions()) + expect(result.count) == 1 + } + + func testIgnoreClasses() { + class AnotherLabel: UILabel { + } + + let sut = UIRedactBuilder() + sut.ignoreClasses.append(AnotherLabel.self) + rootView.addSubview(AnotherLabel(frame: CGRect(x: 20, y: 20, width: 40, height: 40))) + + let result = sut.redactRegionsFor(view: rootView, options: RedactOptions()) + expect(result.count) == 0 + } + +} + +#endif From aba41083abe755c826689bc2e33e7c6dc32fcac2 Mon Sep 17 00:00:00 2001 From: Dhiogo Brustolin Date: Mon, 6 May 2024 10:58:29 -0400 Subject: [PATCH 07/12] chore: Update CHANGELOG (#3942) --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 090f1fa4138..0b5afc1df66 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,10 @@ ### Improvements - Remove not needed lock for logging (#3934) +- Session replay Improvements (#3877) + - Use image average color and text font color to redact session replay + - Removed iOS 16 restriction from session replay + - Performance improvement ## 8.25.0 From 9bc6a96f93d85bf98f0bd5bf981e9e740dcc29e9 Mon Sep 17 00:00:00 2001 From: Philipp Hofmann Date: Mon, 6 May 2024 19:58:44 +0200 Subject: [PATCH 08/12] fix: Stop SessionReplay when closing SDK (#3941) Call SessionReplay.stop when uninstalling the SDK and invalidate the DisplayLinkWrapper when SessionReplay gets deallocated. --- CHANGELOG.md | 1 + Sources/Sentry/SentrySessionReplay.m | 5 +++++ Sources/Sentry/SentrySessionReplayIntegration.m | 1 + .../SessionReplay/SentrySessionReplayTests.swift | 11 +++++++++++ 4 files changed, 18 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0b5afc1df66..a9982141c0a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ - Ignore SentryFramesTracker thread sanitizer data races (#3922) - Handle no releaseName in WatchDogTerminationLogic (#3919) +- Stop SessionReplay when closing SDK (#3941) ### Improvements diff --git a/Sources/Sentry/SentrySessionReplay.m b/Sources/Sentry/SentrySessionReplay.m index 428cc2fdd67..aeb763c9fa9 100644 --- a/Sources/Sentry/SentrySessionReplay.m +++ b/Sources/Sentry/SentrySessionReplay.m @@ -114,6 +114,11 @@ - (void)stop } } +- (void)dealloc +{ + [self stop]; +} + - (void)captureReplayForEvent:(SentryEvent *)event; { if (!_isRunning) { diff --git a/Sources/Sentry/SentrySessionReplayIntegration.m b/Sources/Sentry/SentrySessionReplayIntegration.m index df7a7dd56f2..bb08c45d3fe 100644 --- a/Sources/Sentry/SentrySessionReplayIntegration.m +++ b/Sources/Sentry/SentrySessionReplayIntegration.m @@ -139,6 +139,7 @@ - (SentryIntegrationOption)integrationOptions - (void)uninstall { + [self stop]; } - (BOOL)shouldReplayFullSession:(CGFloat)rate diff --git a/Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayTests.swift b/Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayTests.swift index a21a260ca7b..1cccec763b2 100644 --- a/Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayTests.swift +++ b/Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayTests.swift @@ -208,6 +208,17 @@ class SentrySessionReplayTests: XCTestCase { expect(Dynamic(sut).isRunning) == false } + + @available(iOS 16.0, tvOS 16, *) + func testDealloc_CallsStop() { + let fixture = startFixture() + func sutIsDeallocatedAfterCallingMe() { + _ = fixture.getSut(options: SentryReplayOptions(sessionSampleRate: 1, errorSampleRate: 1)) + } + sutIsDeallocatedAfterCallingMe() + + expect(fixture.displayLink.invalidateInvocations.count) == 1 + } func assertFullSession(_ sessionReplay: SentrySessionReplay, expected: Bool) { expect(Dynamic(sessionReplay).isFullSession) == expected From 7014cc9e14dec5f9a01796fd59cb8dbf76d973f1 Mon Sep 17 00:00:00 2001 From: Philipp Hofmann Date: Tue, 7 May 2024 08:30:35 +0200 Subject: [PATCH 09/12] test: Remove unit test performance tests (#3939) Performance unit tests require baseline profiles bound to an exact machine configuration, which we can't generate for CI. Thus, the performance tests run without any benefit in CI. We can remove them with this PR. --- .../SentryFramesTrackerTests.swift | 14 ------ .../Networking/SentryHttpTransportTests.swift | 48 +++++++------------ Tests/SentryTests/SentryScopeSwiftTests.swift | 25 ---------- 3 files changed, 17 insertions(+), 70 deletions(-) diff --git a/Tests/SentryTests/Integrations/Performance/FramesTracking/SentryFramesTrackerTests.swift b/Tests/SentryTests/Integrations/Performance/FramesTracking/SentryFramesTrackerTests.swift index 11a974dacc9..5a9cff7c03b 100644 --- a/Tests/SentryTests/Integrations/Performance/FramesTracking/SentryFramesTrackerTests.swift +++ b/Tests/SentryTests/Integrations/Performance/FramesTracking/SentryFramesTrackerTests.swift @@ -156,20 +156,6 @@ class SentryFramesTrackerTests: XCTestCase { try assert(slow: 1, frozen: 1, total: 2, frameRates: 2) } - - func testPerformanceOfTrackingFrames() throws { - let sut = fixture.sut - sut.start() - - let frames: UInt = 1_000 - self.measure { - for _ in 0 ..< frames { - fixture.displayLinkWrapper.normalFrame() - } - } - - try assert(slow: 0, frozen: 0) - } /** * The following test validates one slow and one frozen frame in the time interval. The slow frame starts at diff --git a/Tests/SentryTests/Networking/SentryHttpTransportTests.swift b/Tests/SentryTests/Networking/SentryHttpTransportTests.swift index 291692a16f0..93a224bb114 100644 --- a/Tests/SentryTests/Networking/SentryHttpTransportTests.swift +++ b/Tests/SentryTests/Networking/SentryHttpTransportTests.swift @@ -1,3 +1,4 @@ +import Nimble @testable import Sentry import SentryTestUtils import XCTest @@ -557,42 +558,27 @@ class SentryHttpTransportTests: XCTestCase { XCTAssertEqual(1, attachment?.quantity) } - func testPerformanceOfSending() { - self.measure { - givenNoInternetConnection() - for _ in 0...5 { - sendEventAsync() - } - givenOkResponse() - for _ in 0...5 { - sendEventAsync() - } - } - } - func testSendEnvelopesConcurrent() { - self.measure { - fixture.requestManager.responseDelay = 0.0001 - - let queue = fixture.queue - - let group = DispatchGroup() - for _ in 0...20 { - group.enter() - queue.async { - self.givenRecordedLostEvents() - self.sendEventAsync() - group.leave() - } - } + fixture.requestManager.responseDelay = 0.0001 - queue.activate() - group.waitWithTimeout() + let queue = fixture.queue - waitForAllRequests() + let group = DispatchGroup() + for _ in 0...20 { + group.enter() + queue.async { + self.givenRecordedLostEvents() + self.sendEventAsync() + group.leave() + } } - XCTAssertEqual(210, fixture.requestManager.requests.count) + queue.activate() + group.waitWithTimeout() + + waitForAllRequests() + + expect(self.fixture.requestManager.requests.count) == 21 } func testBuildingRequestFails_DeletesEnvelopeAndSendsNext() { diff --git a/Tests/SentryTests/SentryScopeSwiftTests.swift b/Tests/SentryTests/SentryScopeSwiftTests.swift index d623c38a56a..a553916ed09 100644 --- a/Tests/SentryTests/SentryScopeSwiftTests.swift +++ b/Tests/SentryTests/SentryScopeSwiftTests.swift @@ -408,31 +408,6 @@ class SentryScopeSwiftTests: XCTestCase { XCTAssertEqual(0, scope.attachments.count) } - func testPeformanceOfSyncToSentryCrash() { - // To avoid spamming the test logs - SentryLog.configure(true, diagnosticLevel: .error) - - let scope = fixture.scope - scope.add(SentryCrashScopeObserver(maxBreadcrumbs: 100)) - - self.measure { - modifyScope(scope: scope) - } - - setTestDefaultLogLevel() - } - - func testPeformanceOfSyncToSentryCrash_OneCrumb() { - let scope = fixture.scope - scope.add(SentryCrashScopeObserver(maxBreadcrumbs: 100)) - - modifyScope(scope: scope) - - self.measure { - scope.addBreadcrumb(self.fixture.breadcrumb) - } - } - // With this test we test if modifications from multiple threads don't lead to a crash. func testModifyingFromMultipleThreads() { let scope = fixture.scope From 523915af172658aa97eb7b797a171dbf9e227c12 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 7 May 2024 08:47:04 +0200 Subject: [PATCH 10/12] chore(deps): bump codecov/codecov-action from 4.3.0 to 4.3.1 (#3945) Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 4.3.0 to 4.3.1. - [Release notes](https://github.com/codecov/codecov-action/releases) - [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/codecov/codecov-action/compare/84508663e988701840491b86de86b666e8a86bed...5ecb98a3c6b747ed38dc09f787459979aebb39be) --- updated-dependencies: - dependency-name: codecov/codecov-action dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/test.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e3b00ac62d2..ae098939acd 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -238,7 +238,7 @@ jobs: # We don't upload codecov for scheduled runs as CodeCov only accepts a limited amount of uploads per commit. - name: Push code coverage to codecov id: codecov_1 - uses: codecov/codecov-action@84508663e988701840491b86de86b666e8a86bed # pin@v4.3.0 + uses: codecov/codecov-action@5ecb98a3c6b747ed38dc09f787459979aebb39be # pin@v4.3.1 if: ${{ contains(matrix.platform, 'iOS') && !contains(github.ref, 'release') && github.event.schedule == '' }} with: # Although public repos should not have to specify a token there seems to be a bug with the Codecov GH action, which can @@ -250,7 +250,7 @@ jobs: # Sometimes codecov uploads etc can fail. Retry one time to rule out e.g. intermittent network failures. - name: Push code coverage to codecov id: codecov_2 - uses: codecov/codecov-action@84508663e988701840491b86de86b666e8a86bed # pin@v4.3.0 + uses: codecov/codecov-action@5ecb98a3c6b747ed38dc09f787459979aebb39be # pin@v4.3.1 if: ${{ steps.codecov_1.outcome == 'failure' && contains(matrix.platform, 'iOS') && !contains(github.ref, 'release') && github.event.schedule == '' }} with: token: ${{ secrets.CODECOV_TOKEN }} From 99ab5d0e7f9a9b96b0a5876a380eafc7d9dac967 Mon Sep 17 00:00:00 2001 From: Dipak Kasabwala Date: Tue, 7 May 2024 05:17:54 -0400 Subject: [PATCH 11/12] feat: Add option to use own NSURLSession for transport (#3811) Add an option to use their own NSURLSession for transports so users can configure a proxy and such. Fixes GH-3052 Co-authored-by: Philipp Hofmann --- CHANGELOG.md | 4 +++ Sources/Sentry/Public/SentryOptions.h | 13 +++++++++ Sources/Sentry/SentryOptions.m | 4 +++ Sources/Sentry/SentryTransportFactory.m | 17 ++++++++---- .../SentryTransportFactoryTests.swift | 27 +++++++++++++++++++ Tests/SentryTests/SentryOptionsTest.m | 12 +++++++++ 6 files changed, 72 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a9982141c0a..3bd81883e6a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### Features + +- Add option to use own NSURLSession for transport (#3811) + ### Fixes - Ignore SentryFramesTracker thread sanitizer data races (#3922) diff --git a/Sources/Sentry/Public/SentryOptions.h b/Sources/Sentry/Public/SentryOptions.h index 8f67f545c8d..2520778de62 100644 --- a/Sources/Sentry/Public/SentryOptions.h +++ b/Sources/Sentry/Public/SentryOptions.h @@ -358,9 +358,22 @@ NS_SWIFT_NAME(Options) /** * Set as delegate on the @c NSURLSession used for all network data-transfer tasks performed by * Sentry. + * + * @discussion The SDK ignores this option when using @c urlSession. */ @property (nullable, nonatomic, weak) id urlSessionDelegate; +/** + * Use this property, so the transport uses this @c NSURLSession with your configuration for + * sending requests to Sentry. + * + * If not set, the SDK will create a new @c NSURLSession with @c [NSURLSessionConfiguration + * ephemeralSessionConfiguration]. + * + * @note Default is @c nil. + */ +@property (nullable, nonatomic, strong) NSURLSession *urlSession; + /** * Wether the SDK should use swizzling or not. * @discussion When turned off the following features are disabled: breadcrumbs for touch events and diff --git a/Sources/Sentry/SentryOptions.m b/Sources/Sentry/SentryOptions.m index 8e114e7773f..cefb745d22d 100644 --- a/Sources/Sentry/SentryOptions.m +++ b/Sources/Sentry/SentryOptions.m @@ -446,6 +446,10 @@ - (BOOL)validateOptions:(NSDictionary *)options _inAppExcludes = [options[@"inAppExcludes"] filteredArrayUsingPredicate:isNSString]; } + if ([options[@"urlSession"] isKindOfClass:[NSURLSession class]]) { + self.urlSession = options[@"urlSession"]; + } + if ([options[@"urlSessionDelegate"] conformsToProtocol:@protocol(NSURLSessionDelegate)]) { self.urlSessionDelegate = options[@"urlSessionDelegate"]; } diff --git a/Sources/Sentry/SentryTransportFactory.m b/Sources/Sentry/SentryTransportFactory.m index 3ee49f8e8e5..4f7a03b047b 100644 --- a/Sources/Sentry/SentryTransportFactory.m +++ b/Sources/Sentry/SentryTransportFactory.m @@ -28,11 +28,18 @@ @implementation SentryTransportFactory sentryFileManager:(SentryFileManager *)sentryFileManager currentDateProvider:(SentryCurrentDateProvider *)currentDateProvider { - NSURLSessionConfiguration *configuration = - [NSURLSessionConfiguration ephemeralSessionConfiguration]; - NSURLSession *session = [NSURLSession sessionWithConfiguration:configuration - delegate:options.urlSessionDelegate - delegateQueue:nil]; + NSURLSession *session; + + if (options.urlSession) { + session = options.urlSession; + } else { + NSURLSessionConfiguration *configuration = + [NSURLSessionConfiguration ephemeralSessionConfiguration]; + session = [NSURLSession sessionWithConfiguration:configuration + delegate:options.urlSessionDelegate + delegateQueue:nil]; + } + id requestManager = [[SentryQueueableRequestManager alloc] initWithSession:session]; diff --git a/Tests/SentryTests/Networking/SentryTransportFactoryTests.swift b/Tests/SentryTests/Networking/SentryTransportFactoryTests.swift index 7a2b9436708..d7cbee9652e 100644 --- a/Tests/SentryTests/Networking/SentryTransportFactoryTests.swift +++ b/Tests/SentryTests/Networking/SentryTransportFactoryTests.swift @@ -31,6 +31,33 @@ class SentryTransportFactoryTests: XCTestCase { wait(for: [expect], timeout: 10) } + func testShouldReturnTransports_WhenURLSessionPassed() throws { + + let urlSessionDelegateSpy = UrlSessionDelegateSpy() + let expect = expectation(description: "UrlSession Delegate of Options called in RequestManager") + + let sessionConfiguration = URLSession(configuration: .ephemeral, delegate: urlSessionDelegateSpy, delegateQueue: nil) + urlSessionDelegateSpy.delegateCallback = { + expect.fulfill() + } + + let options = Options() + options.urlSession = sessionConfiguration + + let fileManager = try! SentryFileManager(options: options, dispatchQueueWrapper: TestSentryDispatchQueueWrapper()) + let transports = TransportInitializer.initTransports(options, sentryFileManager: fileManager, currentDateProvider: TestCurrentDateProvider()) + + let httpTransport = transports.first + let requestManager = Dynamic(httpTransport).requestManager.asObject as! SentryQueueableRequestManager + + let imgUrl = URL(string: "https://github.com")! + let request = URLRequest(url: imgUrl) + + requestManager.add(request) { _, _ in /* We don't care about the result */ } + wait(for: [expect], timeout: 10) + + } + func testShouldReturnTwoTransports_WhenSpotlightEnabled() throws { let options = Options() options.enableSpotlight = true diff --git a/Tests/SentryTests/SentryOptionsTest.m b/Tests/SentryTests/SentryOptionsTest.m index 8e345e638ff..a3ff293df8f 100644 --- a/Tests/SentryTests/SentryOptionsTest.m +++ b/Tests/SentryTests/SentryOptionsTest.m @@ -518,6 +518,7 @@ - (void)testEmptyConstructorSetsDefaultValues - (void)testNSNull_SetsDefaultValue { SentryOptions *options = [[SentryOptions alloc] initWithDict:@{ + @"urlSession" : [NSNull null], @"dsn" : [NSNull null], @"enabled" : [NSNull null], @"debug" : [NSNull null], @@ -617,6 +618,7 @@ - (void)assertDefaultValues:(SentryOptions *)options XCTAssertEqualObjects([self getDefaultInAppIncludes], options.inAppIncludes); XCTAssertEqual(@[], options.inAppExcludes); XCTAssertNil(options.urlSessionDelegate); + XCTAssertNil(options.urlSession); XCTAssertEqual(YES, options.enableSwizzling); XCTAssertEqual([NSSet new], options.swizzleClassNameExcludes); XCTAssertEqual(YES, options.enableFileIOTracing); @@ -1261,6 +1263,16 @@ - (SentryOptions *)getValidOptions:(NSDictionary *)dict return sentryOptions; } +- (void)testURLSession +{ + NSURLSession *urlSession = [NSURLSession + sessionWithConfiguration:[NSURLSessionConfiguration ephemeralSessionConfiguration]]; + + SentryOptions *options = [self getValidOptions:@{ @"urlSession" : urlSession }]; + + XCTAssertNotNil(options.urlSession); +} + - (void)testUrlSessionDelegate { id urlSessionDelegate = [[UrlSessionDelegateSpy alloc] init]; From cbd77256d1229ff6ca0cc60d94bc69560179ea37 Mon Sep 17 00:00:00 2001 From: Max Chuquimia <2460248+maxchuquimia@users.noreply.github.com> Date: Tue, 7 May 2024 22:11:22 +1000 Subject: [PATCH 12/12] feat: Send GraphQL "operationName" in HTTP breadcrumbs (#3931) This PR attempts to support sending GraphQL operation names with existing HTTP breadcrumbs. Co-authored-by: Max Chuquimia <> Co-authored-by: Philipp Hofmann --- CHANGELOG.md | 1 + Sentry.xcodeproj/project.pbxproj | 9 +++ Sources/Sentry/Public/SentryOptions.h | 6 ++ Sources/Sentry/SentryNetworkTracker.m | 19 +++++ .../Sentry/SentryNetworkTrackingIntegration.m | 4 ++ Sources/Sentry/SentryOptions.m | 4 ++ Sources/Sentry/include/SentryNetworkTracker.h | 2 + .../Extensions/URLSessionTaskExtensions.swift | 19 +++++ ...SentryNetworkTrackerIntegrationTests.swift | 15 +++- .../Network/SentryNetworkTrackerTests.swift | 47 +++++++++++- Tests/SentryTests/SentryOptionsTest.m | 5 ++ .../Extensions/URLSessionTaskTests.swift | 71 +++++++++++++++++++ 12 files changed, 198 insertions(+), 4 deletions(-) create mode 100644 Sources/Swift/Extensions/URLSessionTaskExtensions.swift create mode 100644 Tests/SentryTests/Swift/Extensions/URLSessionTaskTests.swift diff --git a/CHANGELOG.md b/CHANGELOG.md index 3bd81883e6a..6fc9475c826 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ ### Features - Add option to use own NSURLSession for transport (#3811) +- Support sending GraphQL operation names in HTTP breadcrumbs (#3931) ### Fixes diff --git a/Sentry.xcodeproj/project.pbxproj b/Sentry.xcodeproj/project.pbxproj index d8b8e46c741..89ad8d24dc0 100644 --- a/Sentry.xcodeproj/project.pbxproj +++ b/Sentry.xcodeproj/project.pbxproj @@ -72,6 +72,8 @@ 15E0A8F22411A45A00F044E3 /* SentrySession.m in Sources */ = {isa = PBXBuildFile; fileRef = 15E0A8F12411A45A00F044E3 /* SentrySession.m */; }; 33042A0D29DAF79A00C60085 /* SentryExtraContextProvider.m in Sources */ = {isa = PBXBuildFile; fileRef = 33042A0C29DAF79A00C60085 /* SentryExtraContextProvider.m */; }; 33042A1729DC2C4300C60085 /* SentryExtraContextProviderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33042A1629DC2C4300C60085 /* SentryExtraContextProviderTests.swift */; }; + 51B15F7E2BE88A7C0026A2F2 /* URLSessionTaskExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51B15F7D2BE88A7C0026A2F2 /* URLSessionTaskExtensions.swift */; }; + 51B15F802BE88D510026A2F2 /* URLSessionTaskTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51B15F7F2BE88D510026A2F2 /* URLSessionTaskTests.swift */; }; 620379DB2AFE1415005AC0C1 /* SentryBuildAppStartSpans.h in Headers */ = {isa = PBXBuildFile; fileRef = 620379DA2AFE1415005AC0C1 /* SentryBuildAppStartSpans.h */; }; 620379DD2AFE1432005AC0C1 /* SentryBuildAppStartSpans.m in Sources */ = {isa = PBXBuildFile; fileRef = 620379DC2AFE1432005AC0C1 /* SentryBuildAppStartSpans.m */; }; 621D9F2F2B9B0320003D94DE /* SentryCurrentDateProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 621D9F2E2B9B0320003D94DE /* SentryCurrentDateProvider.swift */; }; @@ -1026,6 +1028,8 @@ 33042A0B29DAF5F400C60085 /* SentryExtraContextProvider.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SentryExtraContextProvider.h; sourceTree = ""; }; 33042A0C29DAF79A00C60085 /* SentryExtraContextProvider.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryExtraContextProvider.m; sourceTree = ""; }; 33042A1629DC2C4300C60085 /* SentryExtraContextProviderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryExtraContextProviderTests.swift; sourceTree = ""; }; + 51B15F7D2BE88A7C0026A2F2 /* URLSessionTaskExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = URLSessionTaskExtensions.swift; sourceTree = ""; }; + 51B15F7F2BE88D510026A2F2 /* URLSessionTaskTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLSessionTaskTests.swift; sourceTree = ""; }; 620379DA2AFE1415005AC0C1 /* SentryBuildAppStartSpans.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SentryBuildAppStartSpans.h; path = include/SentryBuildAppStartSpans.h; sourceTree = ""; }; 620379DC2AFE1432005AC0C1 /* SentryBuildAppStartSpans.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryBuildAppStartSpans.m; sourceTree = ""; }; 621D9F2E2B9B0320003D94DE /* SentryCurrentDateProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryCurrentDateProvider.swift; sourceTree = ""; }; @@ -2085,6 +2089,7 @@ isa = PBXGroup; children = ( 62872B622BA1B86100A4FA7D /* NSLockTests.swift */, + 51B15F7F2BE88D510026A2F2 /* URLSessionTaskTests.swift */, ); path = Extensions; sourceTree = ""; @@ -3783,6 +3788,7 @@ children = ( D8F016B52B962548007B9AFB /* StringExtensions.swift */, 62872B5E2BA1B7F300A4FA7D /* NSLock.swift */, + 51B15F7D2BE88A7C0026A2F2 /* URLSessionTaskExtensions.swift */, ); path = Extensions; sourceTree = ""; @@ -4043,6 +4049,7 @@ 639FCFA81EBC80CC00778193 /* SentryFrame.h in Headers */, D8BFE37229A3782F002E73F3 /* SentryTimeToDisplayTracker.h in Headers */, 8E8C57A625EEFC43001CEEFA /* SentrySampling.h in Headers */, + 8E8C57A625EEFC43001CEEFA /* SentrySampling.h in Headers */, 7B634599280EB9D100CFA05A /* SentryUIEventTrackingIntegration.h in Headers */, 63FE716D20DA4C1100CDBAE8 /* SentryCrashSysCtl.h in Headers */, 639889BB1EDED18400EA7442 /* SentrySwizzle.h in Headers */, @@ -4387,6 +4394,7 @@ 7B3B473825D6CC7E00D01640 /* SentryNSError.m in Sources */, D8ACE3C82762187200F5A213 /* SentryNSDataTracker.m in Sources */, 7BE3C77D2446112C00A38442 /* SentryRateLimitParser.m in Sources */, + 51B15F7E2BE88A7C0026A2F2 /* URLSessionTaskExtensions.swift in Sources */, D8B088B729C9E3FF00213258 /* SentryTracerConfiguration.m in Sources */, 8ECC674A25C23A20000E2BF6 /* SentryTransactionContext.mm in Sources */, 03BCC38C27E1C01A003232C7 /* SentryTime.mm in Sources */, @@ -4722,6 +4730,7 @@ 63FE721420DA66EC00CDBAE8 /* SentryCrashMemory_Tests.m in Sources */, 62885DA729E946B100554F38 /* TestConncurrentModifications.swift in Sources */, 63FE720520DA66EC00CDBAE8 /* FileBasedTestCase.m in Sources */, + 51B15F802BE88D510026A2F2 /* URLSessionTaskTests.swift in Sources */, 63EED6C32237989300E02400 /* SentryOptionsTest.m in Sources */, 7BBD18B22451804C00427C76 /* SentryRetryAfterHeaderParserTests.swift in Sources */, 7BD337E424A356180050DB6E /* SentryCrashIntegrationTests.swift in Sources */, diff --git a/Sources/Sentry/Public/SentryOptions.h b/Sources/Sentry/Public/SentryOptions.h index 2520778de62..c1019db4d01 100644 --- a/Sources/Sentry/Public/SentryOptions.h +++ b/Sources/Sentry/Public/SentryOptions.h @@ -142,6 +142,12 @@ NS_SWIFT_NAME(Options) */ @property (nonatomic, assign) BOOL enableAutoSessionTracking; +/** + * Whether to attach the top level `operationName` node of HTTP json requests to HTTP breadcrumbs + * @note Default is @c NO. + */ +@property (nonatomic, assign) BOOL enableGraphQLOperationTracking; + /** * Whether to enable Watchdog Termination tracking or not. * @note This feature requires the @c SentryCrashIntegration being enabled, otherwise it would diff --git a/Sources/Sentry/SentryNetworkTracker.m b/Sources/Sentry/SentryNetworkTracker.m index b55d0876891..93287f991c6 100644 --- a/Sources/Sentry/SentryNetworkTracker.m +++ b/Sources/Sentry/SentryNetworkTracker.m @@ -43,6 +43,7 @@ @property (nonatomic, assign) BOOL isNetworkTrackingEnabled; @property (nonatomic, assign) BOOL isNetworkBreadcrumbEnabled; @property (nonatomic, assign) BOOL isCaptureFailedRequestsEnabled; +@property (nonatomic, assign) BOOL isGraphQLOperationTrackingEnabled; @end @@ -62,6 +63,7 @@ - (instancetype)init _isNetworkTrackingEnabled = NO; _isNetworkBreadcrumbEnabled = NO; _isCaptureFailedRequestsEnabled = NO; + _isGraphQLOperationTrackingEnabled = NO; } return self; } @@ -87,12 +89,20 @@ - (void)enableCaptureFailedRequests } } +- (void)enableGraphQLOperationTracking +{ + @synchronized(self) { + _isGraphQLOperationTrackingEnabled = YES; + } +} + - (void)disable { @synchronized(self) { _isNetworkBreadcrumbEnabled = NO; _isNetworkTrackingEnabled = NO; _isCaptureFailedRequestsEnabled = NO; + _isGraphQLOperationTrackingEnabled = NO; } } @@ -440,6 +450,11 @@ - (void)captureFailedRequests:(NSURLSessionTask *)sessionTask } context[@"response"] = response; + + if (self.isGraphQLOperationTrackingEnabled) { + context[@"graphql_operation_name"] = [sessionTask getGraphQLOperationName]; + } + event.context = context; [SentrySDK captureEvent:event]; @@ -489,6 +504,10 @@ - (void)addBreadcrumbForSessionTask:(NSURLSessionTask *)sessionTask breadcrumbData[@"status_code"] = statusCode; breadcrumbData[@"reason"] = [NSHTTPURLResponse localizedStringForStatusCode:responseStatusCode]; + + if (self.isGraphQLOperationTrackingEnabled) { + breadcrumbData[@"graphql_operation_name"] = [sessionTask getGraphQLOperationName]; + } } if (urlComponents.query != nil) { diff --git a/Sources/Sentry/SentryNetworkTrackingIntegration.m b/Sources/Sentry/SentryNetworkTrackingIntegration.m index 63ee302eb7b..5c70c80a2d5 100644 --- a/Sources/Sentry/SentryNetworkTrackingIntegration.m +++ b/Sources/Sentry/SentryNetworkTrackingIntegration.m @@ -29,6 +29,10 @@ - (BOOL)installWithOptions:(SentryOptions *)options [SentryNetworkTracker.sharedInstance enableCaptureFailedRequests]; } + if (options.enableGraphQLOperationTracking) { + [SentryNetworkTracker.sharedInstance enableGraphQLOperationTracking]; + } + if (shouldEnableNetworkTracking || options.enableNetworkBreadcrumbs || options.enableCaptureFailedRequests) { [SentryNetworkTrackingIntegration swizzleURLSessionTask]; diff --git a/Sources/Sentry/SentryOptions.m b/Sources/Sentry/SentryOptions.m index cefb745d22d..e6ca2aaa6fb 100644 --- a/Sources/Sentry/SentryOptions.m +++ b/Sources/Sentry/SentryOptions.m @@ -94,6 +94,7 @@ - (instancetype)init _integrations = SentryOptions.defaultIntegrations; self.sampleRate = SENTRY_DEFAULT_SAMPLE_RATE; self.enableAutoSessionTracking = YES; + self.enableGraphQLOperationTracking = NO; self.enableWatchdogTerminationTracking = YES; self.sessionTrackingIntervalMillis = [@30000 unsignedIntValue]; self.attachStacktrace = YES; @@ -353,6 +354,9 @@ - (BOOL)validateOptions:(NSDictionary *)options [self setBool:options[@"enableAutoSessionTracking"] block:^(BOOL value) { self->_enableAutoSessionTracking = value; }]; + [self setBool:options[@"enableGraphQLOperationTracking"] + block:^(BOOL value) { self->_enableGraphQLOperationTracking = value; }]; + [self setBool:options[@"enableWatchdogTerminationTracking"] block:^(BOOL value) { self->_enableWatchdogTerminationTracking = value; }]; diff --git a/Sources/Sentry/include/SentryNetworkTracker.h b/Sources/Sentry/include/SentryNetworkTracker.h index 237e55dbd43..e0aa040da81 100644 --- a/Sources/Sentry/include/SentryNetworkTracker.h +++ b/Sources/Sentry/include/SentryNetworkTracker.h @@ -18,12 +18,14 @@ static NSString *const SENTRY_NETWORK_REQUEST_TRACKER_BREADCRUMB - (void)enableNetworkTracking; - (void)enableNetworkBreadcrumbs; - (void)enableCaptureFailedRequests; +- (void)enableGraphQLOperationTracking; - (BOOL)isTargetMatch:(NSURL *)URL withTargets:(NSArray *)targets; - (void)disable; @property (nonatomic, readonly) BOOL isNetworkTrackingEnabled; @property (nonatomic, readonly) BOOL isNetworkBreadcrumbEnabled; @property (nonatomic, readonly) BOOL isCaptureFailedRequestsEnabled; +@property (nonatomic, readonly) BOOL isGraphQLOperationTrackingEnabled; @end diff --git a/Sources/Swift/Extensions/URLSessionTaskExtensions.swift b/Sources/Swift/Extensions/URLSessionTaskExtensions.swift new file mode 100644 index 00000000000..acf7bda79f0 --- /dev/null +++ b/Sources/Swift/Extensions/URLSessionTaskExtensions.swift @@ -0,0 +1,19 @@ +import Foundation + +public extension URLSessionTask { + + @objc + func getGraphQLOperationName() -> String? { + guard originalRequest?.value(forHTTPHeaderField: "Content-Type") == "application/json" else { return nil } + guard let requestBody = originalRequest?.httpBody else { return nil } + + let requestInfo = try? JSONDecoder().decode(GraphQLRequest.self, from: requestBody) + + return requestInfo?.operationName + } + +} + +private struct GraphQLRequest: Decodable { + let operationName: String +} diff --git a/Tests/SentryTests/Integrations/Performance/Network/SentryNetworkTrackerIntegrationTests.swift b/Tests/SentryTests/Integrations/Performance/Network/SentryNetworkTrackerIntegrationTests.swift index bff5d482fa7..3b8f0e0b3c8 100644 --- a/Tests/SentryTests/Integrations/Performance/Network/SentryNetworkTrackerIntegrationTests.swift +++ b/Tests/SentryTests/Integrations/Performance/Network/SentryNetworkTrackerIntegrationTests.swift @@ -228,7 +228,20 @@ class SentryNetworkTrackerIntegrationTests: XCTestCase { XCTAssertFalse(SentryNetworkTracker.sharedInstance.isCaptureFailedRequestsEnabled) } - + + func testGraphQLOperationTrackingEnabled() { + fixture.options.enableGraphQLOperationTracking = true + startSDK() + + XCTAssertTrue(SentryNetworkTracker.sharedInstance.isGraphQLOperationTrackingEnabled) + } + + func testGraphQLOperationTrackingDisabled() { + startSDK() + + XCTAssertFalse(SentryNetworkTracker.sharedInstance.isGraphQLOperationTrackingEnabled) + } + func testGetCaptureFailedRequestsEnabled() { let expect = expectation(description: "Request completed") diff --git a/Tests/SentryTests/Integrations/Performance/Network/SentryNetworkTrackerTests.swift b/Tests/SentryTests/Integrations/Performance/Network/SentryNetworkTrackerTests.swift index 4c75ec7c8f5..96cbc56aaa5 100644 --- a/Tests/SentryTests/Integrations/Performance/Network/SentryNetworkTrackerTests.swift +++ b/Tests/SentryTests/Integrations/Performance/Network/SentryNetworkTrackerTests.swift @@ -17,7 +17,7 @@ class SentryNetworkTrackerTests: XCTestCase { let dateProvider = TestCurrentDateProvider() let options: Options let scope: Scope - let nsUrlRequest = NSURLRequest(url: SentryNetworkTrackerTests.fullUrl) + let nsUrlRequest = NSMutableURLRequest(url: SentryNetworkTrackerTests.fullUrl) let client: TestClient! let hub: TestHub! let securityHeader = [ "X-FORWARDED-FOR": "value", @@ -48,6 +48,7 @@ class SentryNetworkTrackerTests: XCTestCase { result.enableNetworkTracking() result.enableNetworkBreadcrumbs() result.enableCaptureFailedRequests() + result.enableGraphQLOperationTracking() return result } } @@ -337,8 +338,41 @@ class SentryNetworkTrackerTests: XCTestCase { XCTAssertEqual(breadcrumb!.data!["response_body_size"] as! Int64, DATA_BYTES_RECEIVED) XCTAssertEqual(breadcrumb!.data!["http.query"] as? String, "query=value&query2=value2") XCTAssertEqual(breadcrumb!.data!["http.fragment"] as? String, "fragment") + XCTAssertNil(breadcrumb!.data!["graphql_operation_name"]) } - + + func testBreadcrumb_GraphQLEnabled() { + let body = """ + { + "operationName": "someOperationName", + "variables":{"a": 1}, + "query":"query someOperationName {\\n someField\\n}\\n" + } + """ + fixture.nsUrlRequest.httpBody = body.data(using: .utf8) + fixture.nsUrlRequest.setValue("application/json", forHTTPHeaderField: "content-type") + assertStatus(status: .ok, state: .completed, response: createResponse(code: 200)) + + let breadcrumbs = Dynamic(fixture.scope).breadcrumbArray as [Breadcrumb]? + let breadcrumb = breadcrumbs!.first + XCTAssertEqual(breadcrumb!.data!["graphql_operation_name"] as? String, "someOperationName") + } + + func testBreadcrumb_GraphQLEnabledInvalidData() { + let body = """ + [ + {"message": "arrays are valid json"} + ] + """ + fixture.nsUrlRequest.httpBody = body.data(using: .utf8) + fixture.nsUrlRequest.setValue("application/json", forHTTPHeaderField: "content-type") + assertStatus(status: .ok, state: .completed, response: createResponse(code: 200)) + + let breadcrumbs = Dynamic(fixture.scope).breadcrumbArray as [Breadcrumb]? + let breadcrumb = breadcrumbs!.first + XCTAssertNil(breadcrumb!.data!["graphql_operation_name"]) + } + func testNoBreadcrumb_DisablingBreadcrumb() { assertStatus(status: .ok, state: .completed, response: createResponse(code: 200)) { $0.disable() @@ -868,13 +902,15 @@ class SentryNetworkTrackerTests: XCTestCase { let requestType = span.data["type"] as? String let query = span.data["http.query"] as? String let fragment = span.data["http.fragment"] as? String + let graphql = span.data["graphql_operation_name"] as? String XCTAssertEqual(path, "https://www.domain.com/api") XCTAssertEqual(method, task.currentRequest!.httpMethod) XCTAssertEqual(requestType, "fetch") XCTAssertEqual(query, "query=value&query2=value2") XCTAssertEqual(fragment, "fragment") - + XCTAssertNil(graphql) + XCTAssertEqual(span.status, status) XCTAssertNil(task.observationInfo) } @@ -925,6 +961,11 @@ class SentryNetworkTrackerTests: XCTestCase { func createDataTask(method: String = "GET", modifyRequest: ((URLRequest) -> (URLRequest))? = nil) -> URLSessionDataTaskMock { var request = URLRequest(url: SentryNetworkTrackerTests.fullUrl) request.httpMethod = method + request.httpBody = fixture.nsUrlRequest.httpBody + fixture.nsUrlRequest.allHTTPHeaderFields?.forEach { key, value in + request.setValue(value, forHTTPHeaderField: key) + } + if let modifyRequest = modifyRequest { request = modifyRequest(request) } diff --git a/Tests/SentryTests/SentryOptionsTest.m b/Tests/SentryTests/SentryOptionsTest.m index a3ff293df8f..d50996eb8dc 100644 --- a/Tests/SentryTests/SentryOptionsTest.m +++ b/Tests/SentryTests/SentryOptionsTest.m @@ -200,6 +200,11 @@ - (void)testEnableCoreDataTracking [self testBooleanField:@"enableCoreDataTracing" defaultValue:YES]; } +- (void)testEnableGraphQLOperationTracking +{ + [self testBooleanField:@"enableGraphQLOperationTracking" defaultValue:NO]; +} + - (void)testSendClientReports { [self testBooleanField:@"sendClientReports" defaultValue:YES]; diff --git a/Tests/SentryTests/Swift/Extensions/URLSessionTaskTests.swift b/Tests/SentryTests/Swift/Extensions/URLSessionTaskTests.swift new file mode 100644 index 00000000000..bc97e3ab648 --- /dev/null +++ b/Tests/SentryTests/Swift/Extensions/URLSessionTaskTests.swift @@ -0,0 +1,71 @@ +import Foundation +import Nimble +@testable import Sentry +import XCTest + +final class URLSessionTaskTests: XCTestCase { + + func testHTTPContentTypeInvalid() { + let task = makeTask( + headers: ["Content-Type": "image/jpeg"], + body: "8J+YiQo=" + ) + + let operationName = task.getGraphQLOperationName() + + expect(operationName) == nil + } + + func testHTTPBodyDataInvalid() { + let task = makeTask( + headers: ["Content-Type": "application/json"], + body: "not json" + ) + + let operationName = task.getGraphQLOperationName() + + expect(operationName) == nil + } + + func testHTTPBodyDataMissing() { + let task = makeTask( + headers: ["Content-Type": "application/json"], + body: nil + ) + + let operationName = task.getGraphQLOperationName() + + expect(operationName) == nil + } + + func testHTTPBodyDataValidGraphQL() { + let task = makeTask( + headers: ["Content-Type": "application/json"], + body: """ + { + "operationName": "MyOperation", + "variables": { + "id": "1234" + }, + "query": "query MyOperation($id: ID!) { node(id: $id) { id } }" + } + """ + ) + + let operationName = task.getGraphQLOperationName() + + expect(operationName) == "MyOperation" + } + +} + +private extension URLSessionTaskTests { + + func makeTask(headers: [String: String], body: String?) -> URLSessionTask { + var request = URLRequest(url: URL(string: "https://anything.com")!) + request.httpBody = body?.data(using: .utf8) + request.allHTTPHeaderFields = headers + return URLSession(configuration: .ephemeral).dataTask(with: request) + } + +}