diff --git a/.github/last-release-runid b/.github/last-release-runid index df0826299c..e6c89bac68 100644 --- a/.github/last-release-runid +++ b/.github/last-release-runid @@ -1 +1 @@ -11685387200 +11724928628 diff --git a/.github/workflows/benchmarking.yml b/.github/workflows/benchmarking.yml index 19a800574f..d3eb2b9e7a 100644 --- a/.github/workflows/benchmarking.yml +++ b/.github/workflows/benchmarking.yml @@ -10,7 +10,7 @@ on: - 'Sources/**' # test changes to benchmarking implementation - - 'Samples/iOS-Swift/iOS-Swift/**' + - 'Samples/iOS-Swift/**' - 'Samples/iOS-Swift/PerformanceBenchmarks/**' - '.github/workflows/benchmarking.yml' - '.sauce/benchmarking-config.yml' diff --git a/CHANGELOG.md b/CHANGELOG.md index 4dca7ed324..30f8a2d162 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,20 @@ ## Unreleased +### Features + +- Transactions for crashes (#4504): Finish the transaction bound to the scope when the app crashes. This __experimental__ feature is disabled by default. You can enable it via the option `enablePersistingTracesWhenCrashing`. + +### Fixes + +- Keep PropagationContext when cloning scope (#4518) + +## 8.40.1 + +### Fixes + +- Session replay masking not working inside scroll view (#4498) + ### Improvements - Expose `Sentry._Hybrid` explicit module (#4440) @@ -17,11 +31,13 @@ - Time-of-check time-of-use filesystem race condition (#4473) - Capture all touches with session replay (#4477) + ### Improvements - Improve frames tracker performance (#4469) - Log a warning when dropping envelopes due to rate-limiting (#4463) - Expose `SentrySessionReplayIntegration-Hybrid.h` as `private` (#4486) +- Stops session replay if rate limiting is activated (#4496) - Add `maskedViewClasses` and `unmaskedViewClasses` to SentryReplayOptions init via dict (#4492) - Add `quality` to SentryReplayOptions init via dict (#4495) diff --git a/Package.swift b/Package.swift index 21fd4d10db..d4e73a62a5 100644 --- a/Package.swift +++ b/Package.swift @@ -12,13 +12,13 @@ let package = Package( targets: [ .binaryTarget( name: "Sentry", - url: "https://github.com/getsentry/sentry-cocoa/releases/download/8.40.0/Sentry.xcframework.zip", - checksum: "aa02c15ed98f2560436ccbcc0d25c848f2e8250c28c6c4a01ff3ac4231eb008b" //Sentry-Static + url: "https://github.com/getsentry/sentry-cocoa/releases/download/8.40.1/Sentry.xcframework.zip", + checksum: "db928e6fdc30de1aa97200576d86d467880df710cf5eeb76af23997968d7b2c7" //Sentry-Static ), .binaryTarget( name: "Sentry-Dynamic", - url: "https://github.com/getsentry/sentry-cocoa/releases/download/8.40.0/Sentry-Dynamic.xcframework.zip", - checksum: "a24eeb1737a531d56c8b4ce6f63b2d1fb1a2a0fd05c4eb85a27ece8fe3e442b5" //Sentry-Dynamic + url: "https://github.com/getsentry/sentry-cocoa/releases/download/8.40.1/Sentry-Dynamic.xcframework.zip", + checksum: "31c55833e848a27100d38b607867bb5ce197d7183303b2c4d8ba4fa97d288df3" //Sentry-Dynamic ), .target ( name: "SentrySwiftUI", dependencies: ["Sentry", "SentryInternal"], diff --git a/Samples/iOS-Swift/iOS-Swift.xcodeproj/project.pbxproj b/Samples/iOS-Swift/iOS-Swift.xcodeproj/project.pbxproj index 2427ebafc7..2f8e1fb42f 100644 --- a/Samples/iOS-Swift/iOS-Swift.xcodeproj/project.pbxproj +++ b/Samples/iOS-Swift/iOS-Swift.xcodeproj/project.pbxproj @@ -1415,10 +1415,12 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_ENTITLEMENTS = "iOS-Swift/iOS-Swift.entitlements"; CODE_SIGN_IDENTITY = "iPhone Developer"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; CODE_SIGN_STYLE = Manual; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = 97JCY7859U; + "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 97JCY7859U; INFOPLIST_FILE = "iOS-Swift/Info.plist"; IPHONEOS_DEPLOYMENT_TARGET = 13.0; LD_RUNPATH_SEARCH_PATHS = ( @@ -1428,6 +1430,7 @@ PRODUCT_BUNDLE_IDENTIFIER = "io.sentry.sample.iOS-Swift"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = "match Development io.sentry.sample.iOS-Swift"; + "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "match Development io.sentry.sample.iOS-Swift"; "PROVISIONING_PROFILE_SPECIFIER[sdk=macosx*]" = ""; SUPPORTS_MACCATALYST = YES; SWIFT_OBJC_BRIDGING_HEADER = "iOS-Swift/Tools/iOS-Swift-Bridging-Header.h"; @@ -1629,10 +1632,12 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_ENTITLEMENTS = "iOS-Swift/iOS-Swift.entitlements"; CODE_SIGN_IDENTITY = "iPhone Developer"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; CODE_SIGN_STYLE = Manual; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = 97JCY7859U; + "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 97JCY7859U; INFOPLIST_FILE = "iOS-Swift/Info.plist"; IPHONEOS_DEPLOYMENT_TARGET = 13.0; LD_RUNPATH_SEARCH_PATHS = ( @@ -1642,6 +1647,7 @@ PRODUCT_BUNDLE_IDENTIFIER = "io.sentry.sample.iOS-Swift"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = "match Development io.sentry.sample.iOS-Swift"; + "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "match Development io.sentry.sample.iOS-Swift"; "PROVISIONING_PROFILE_SPECIFIER[sdk=macosx*]" = ""; SUPPORTS_MACCATALYST = YES; SWIFT_OBJC_BRIDGING_HEADER = "iOS-Swift/Tools/iOS-Swift-Bridging-Header.h"; @@ -1705,9 +1711,11 @@ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_ENTITLEMENTS = "iOS-SwiftClip/iOS_SwiftClip.entitlements"; CODE_SIGN_IDENTITY = "iPhone Developer"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = 97JCY7859U; + "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 97JCY7859U; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = "iOS-SwiftClip/Info.plist"; INFOPLIST_KEY_CFBundleDisplayName = "iOS-Swift"; @@ -1726,6 +1734,7 @@ PRODUCT_BUNDLE_IDENTIFIER = "io.sentry.sample.iOS-Swift.Clip"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = "match Development io.sentry.sample.iOS-Swift.Clip"; + "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "match Development io.sentry.sample.iOS-Swift.Clip"; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; @@ -1861,10 +1870,12 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_ENTITLEMENTS = "iOS-Swift/iOS-Swift.entitlements"; CODE_SIGN_IDENTITY = "iPhone Developer"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; CODE_SIGN_STYLE = Manual; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = 97JCY7859U; + "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 97JCY7859U; INFOPLIST_FILE = "iOS-Swift/Info.plist"; IPHONEOS_DEPLOYMENT_TARGET = 13.0; LD_RUNPATH_SEARCH_PATHS = ( @@ -1874,6 +1885,7 @@ PRODUCT_BUNDLE_IDENTIFIER = "io.sentry.sample.iOS-Swift"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = "match Development io.sentry.sample.iOS-Swift"; + "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "match Development io.sentry.sample.iOS-Swift"; "PROVISIONING_PROFILE_SPECIFIER[sdk=macosx*]" = ""; SUPPORTS_MACCATALYST = YES; SWIFT_OBJC_BRIDGING_HEADER = "iOS-Swift/Tools/iOS-Swift-Bridging-Header.h"; @@ -1937,9 +1949,11 @@ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_ENTITLEMENTS = "iOS-SwiftClip/iOS_SwiftClip.entitlements"; CODE_SIGN_IDENTITY = "iPhone Developer"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = 97JCY7859U; + "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 97JCY7859U; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = "iOS-SwiftClip/Info.plist"; INFOPLIST_KEY_CFBundleDisplayName = "iOS-Swift"; @@ -1958,6 +1972,7 @@ PRODUCT_BUNDLE_IDENTIFIER = "io.sentry.sample.iOS-Swift.Clip"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = "match Development io.sentry.sample.iOS-Swift.Clip"; + "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "match Development io.sentry.sample.iOS-Swift.Clip"; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; @@ -2107,9 +2122,11 @@ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_ENTITLEMENTS = "iOS-SwiftClip/iOS_SwiftClip.entitlements"; CODE_SIGN_IDENTITY = "iPhone Developer"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = 97JCY7859U; + "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 97JCY7859U; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = "iOS-SwiftClip/Info.plist"; INFOPLIST_KEY_CFBundleDisplayName = "iOS-Swift"; @@ -2128,6 +2145,7 @@ PRODUCT_BUNDLE_IDENTIFIER = "io.sentry.sample.iOS-Swift.Clip"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = "match Development io.sentry.sample.iOS-Swift.Clip"; + "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "match Development io.sentry.sample.iOS-Swift.Clip"; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; @@ -2141,9 +2159,11 @@ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_ENTITLEMENTS = "iOS-SwiftClip/iOS_SwiftClip.entitlements"; CODE_SIGN_IDENTITY = "iPhone Distribution"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = 97JCY7859U; + "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 97JCY7859U; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = "iOS-SwiftClip/Info.plist"; INFOPLIST_KEY_CFBundleDisplayName = "iOS-Swift"; @@ -2162,6 +2182,7 @@ PRODUCT_BUNDLE_IDENTIFIER = "io.sentry.sample.iOS-Swift.Clip"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = "match AppStore io.sentry.sample.iOS-Swift.Clip"; + "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "match AppStore io.sentry.sample.iOS-Swift.Clip"; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; diff --git a/Samples/iOS-Swift/iOS-Swift/AppDelegate.swift b/Samples/iOS-Swift/iOS-Swift/AppDelegate.swift index a0df2dd3b7..e6b7977da0 100644 --- a/Samples/iOS-Swift/iOS-Swift/AppDelegate.swift +++ b/Samples/iOS-Swift/iOS-Swift/AppDelegate.swift @@ -113,6 +113,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { options.enableSwizzling = !args.contains("--disable-swizzling") options.enableCrashHandler = !args.contains("--disable-crash-handler") options.enableTracing = !args.contains("--disable-tracing") + options.enablePersistingTracesWhenCrashing = true // because we run CPU for 15 seconds at full throttle, we trigger ANR issues being sent. disable such during benchmarks. options.enableAppHangTracking = !isBenchmarking && !args.contains("--disable-anr-tracking") diff --git a/Samples/iOS-Swift/iOS-Swift/ErrorsViewController.swift b/Samples/iOS-Swift/iOS-Swift/ErrorsViewController.swift index f471f6144e..209df3d3a9 100644 --- a/Samples/iOS-Swift/iOS-Swift/ErrorsViewController.swift +++ b/Samples/iOS-Swift/iOS-Swift/ErrorsViewController.swift @@ -28,7 +28,14 @@ class ErrorsViewController: UIViewController { } @IBAction func crash(_ sender: UIButton) { - SentrySDK.crash() + let transaction = SentrySDK.startTransaction(name: "Crashing Transaction", operation: "ui.load", bindToScope: true) + + transaction.startChild(operation: "operation explode") + + DispatchQueue.global().asyncAfter(deadline: .now() + 0.1) { + transaction.startChild(operation: "operation crash") + SentrySDK.crash() + } } // swiftlint:disable force_unwrapping diff --git a/Samples/iOS-Swift/iOS-Swift/Sample.xcconfig b/Samples/iOS-Swift/iOS-Swift/Sample.xcconfig index f94e2a2b39..a96fb6288e 100644 --- a/Samples/iOS-Swift/iOS-Swift/Sample.xcconfig +++ b/Samples/iOS-Swift/iOS-Swift/Sample.xcconfig @@ -1 +1 @@ -MARKETING_VERSION = 8.40.0 +MARKETING_VERSION = 8.40.1 diff --git a/Samples/iOS-Swift/iOS-Swift/ViewControllers/TableViewController.swift b/Samples/iOS-Swift/iOS-Swift/ViewControllers/TableViewController.swift index ae085943e7..2df83233c4 100644 --- a/Samples/iOS-Swift/iOS-Swift/ViewControllers/TableViewController.swift +++ b/Samples/iOS-Swift/iOS-Swift/ViewControllers/TableViewController.swift @@ -36,6 +36,7 @@ class TableViewController: UITableViewController { let w = 1.0 - (Double(indexPath.row) / 99) cell.backgroundColor = UIColor(white: CGFloat(w), alpha: 1) + cell.textLabel?.text = "Row #\(indexPath.row)" return cell } diff --git a/Sentry.podspec b/Sentry.podspec index 524067ed65..a40b974e86 100644 --- a/Sentry.podspec +++ b/Sentry.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = "Sentry" - s.version = "8.40.0" + s.version = "8.40.1" s.summary = "Sentry client for cocoa" s.homepage = "https://github.com/getsentry/sentry-cocoa" s.license = "mit" diff --git a/SentryPrivate.podspec b/SentryPrivate.podspec index 3cb757c6dd..65c1f5b9aa 100644 --- a/SentryPrivate.podspec +++ b/SentryPrivate.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = "SentryPrivate" - s.version = "8.40.0" + s.version = "8.40.1" s.summary = "Sentry Private Library." s.homepage = "https://github.com/getsentry/sentry-cocoa" s.license = "mit" diff --git a/SentrySwiftUI.podspec b/SentrySwiftUI.podspec index ad7b689536..0266a0d0bf 100644 --- a/SentrySwiftUI.podspec +++ b/SentrySwiftUI.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = "SentrySwiftUI" - s.version = "8.40.0" + s.version = "8.40.1" s.summary = "Sentry client for SwiftUI" s.homepage = "https://github.com/getsentry/sentry-cocoa" s.license = "mit" @@ -19,5 +19,5 @@ Pod::Spec.new do |s| s.watchos.framework = 'WatchKit' s.source_files = "Sources/SentrySwiftUI/**/*.{swift,h,m}" - s.dependency 'Sentry', "8.40.0" + s.dependency 'Sentry', "8.40.1" end diff --git a/SentryTestUtils/TestClient.swift b/SentryTestUtils/TestClient.swift index 2629d0f289..eadd215565 100644 --- a/SentryTestUtils/TestClient.swift +++ b/SentryTestUtils/TestClient.swift @@ -108,6 +108,11 @@ public class TestClient: SentryClient { return SentryId() } + public var saveCrashTransactionInvocations = Invocations<(event: Event, scope: Scope)>() + public override func saveCrashTransaction(transaction: Transaction, scope: Scope) { + saveCrashTransactionInvocations.record((transaction, scope)) + } + public var captureUserFeedbackInvocations = Invocations() public override func capture(userFeedback: UserFeedback) { captureUserFeedbackInvocations.record(userFeedback) diff --git a/SentryTestUtils/TestTransport.swift b/SentryTestUtils/TestTransport.swift index 9930713f69..145dbef251 100644 --- a/SentryTestUtils/TestTransport.swift +++ b/SentryTestUtils/TestTransport.swift @@ -2,12 +2,17 @@ import _SentryPrivate import Foundation public class TestTransport: NSObject, Transport { - + public var sentEnvelopes = Invocations() public func send(envelope: SentryEnvelope) { sentEnvelopes.record(envelope) } + public var storedEnvelopes = Invocations() + public func store(_ envelope: SentryEnvelope) { + storedEnvelopes.record(envelope) + } + public var recordLostEvents = Invocations<(category: SentryDataCategory, reason: SentryDiscardReason)>() public func recordLostEvent(_ category: SentryDataCategory, reason: SentryDiscardReason) { recordLostEvents.record((category, reason)) diff --git a/SentryTestUtils/TestTransportAdapter.swift b/SentryTestUtils/TestTransportAdapter.swift index e5c6c513df..c8325a0b21 100644 --- a/SentryTestUtils/TestTransportAdapter.swift +++ b/SentryTestUtils/TestTransportAdapter.swift @@ -19,6 +19,11 @@ public class TestTransportAdapter: SentryTransportAdapter { public override func send(event: Event, traceContext: TraceContext?, attachments: [Attachment], additionalEnvelopeItems: [SentryEnvelopeItem]) { sendEventWithTraceStateInvocations.record((event, traceContext, attachments, additionalEnvelopeItems)) } + + public var storeEventInvocations = Invocations<(event: Event, traceContext: TraceContext?)>() + public override func store(_ event: Event, traceContext: TraceContext?) { + storeEventInvocations.record((event, traceContext)) + } public var userFeedbackInvocations = Invocations() public override func send(userFeedback: UserFeedback) { diff --git a/Sources/Configuration/SDK.xcconfig b/Sources/Configuration/SDK.xcconfig index ce6fe3ff3f..c23053d306 100644 --- a/Sources/Configuration/SDK.xcconfig +++ b/Sources/Configuration/SDK.xcconfig @@ -10,7 +10,7 @@ DYLIB_INSTALL_NAME_BASE = @rpath MACH_O_TYPE = mh_dylib FRAMEWORK_VERSION = A -CURRENT_PROJECT_VERSION = 8.40.0 +CURRENT_PROJECT_VERSION = 8.40.1 ALWAYS_SEARCH_USER_PATHS = NO CLANG_ENABLE_OBJC_ARC = YES diff --git a/Sources/Configuration/SentrySwiftUI.xcconfig b/Sources/Configuration/SentrySwiftUI.xcconfig index 5bcd170836..e1075292ce 100644 --- a/Sources/Configuration/SentrySwiftUI.xcconfig +++ b/Sources/Configuration/SentrySwiftUI.xcconfig @@ -1,5 +1,5 @@ PRODUCT_NAME = SentrySwiftUI -CURRENT_PROJECT_VERSION = 8.40.0 +CURRENT_PROJECT_VERSION = 8.40.1 MACOSX_DEPLOYMENT_TARGET = 10.15 IPHONEOS_DEPLOYMENT_TARGET = 13.0 diff --git a/Sources/Sentry/Public/SentryDefines.h b/Sources/Sentry/Public/SentryDefines.h index 905938c4b2..a6e9e6be2a 100644 --- a/Sources/Sentry/Public/SentryDefines.h +++ b/Sources/Sentry/Public/SentryDefines.h @@ -138,16 +138,6 @@ typedef NSNumber *_Nullable (^SentryTracesSamplerCallback)( */ typedef void (^SentrySpanCallback)(id _Nullable span); -/** - * A callback block which gets called right before a metric is about to be emitted. - - * @param key The key of the metric. - * @param tags A dictionary of key-value pairs associated with the metric. - * @return BOOL YES if the metric should be emitted, NO otherwise. - */ -typedef BOOL (^SentryBeforeEmitMetricCallback)( - NSString *_Nonnull key, NSDictionary *_Nonnull tags); - /** * Log level. */ diff --git a/Sources/Sentry/Public/SentryOptions.h b/Sources/Sentry/Public/SentryOptions.h index 0a053c418e..4ec7bd9a66 100644 --- a/Sources/Sentry/Public/SentryOptions.h +++ b/Sources/Sentry/Public/SentryOptions.h @@ -277,6 +277,17 @@ NS_SWIFT_NAME(Options) */ @property (nonatomic, assign) BOOL enablePerformanceV2; +/** + * @warning This is an experimental feature and may still have bugs. + * + * When enabled, the SDK finishes the ongoing transaction bound to the scope and links them to the + * crash event when your app crashes. The SDK skips adding profiles to increase the chance of + * keeping the transaction. + * + * @note The default is @c NO . + */ +@property (nonatomic, assign) BOOL enablePersistingTracesWhenCrashing; + /** * A block that configures the initial scope when starting the SDK. * @discussion The block receives a suggested default scope. You can either diff --git a/Sources/Sentry/SentryClient.m b/Sources/Sentry/SentryClient.m index a31ac3abff..3e99938100 100644 --- a/Sources/Sentry/SentryClient.m +++ b/Sources/Sentry/SentryClient.m @@ -99,10 +99,10 @@ - (instancetype)initWithOptions:(SentryOptions *)options fileManager:(SentryFileManager *)fileManager deleteOldEnvelopeItems:(BOOL)deleteOldEnvelopeItems { - NSArray> *transports = [SentryTransportFactory - initTransports:options - sentryFileManager:fileManager - currentDateProvider:SentryDependencyContainer.sharedInstance.dateProvider]; + NSArray> *transports = + [SentryTransportFactory initTransports:options + sentryFileManager:fileManager + rateLimits:SentryDependencyContainer.sharedInstance.rateLimits]; SentryTransportAdapter *transportAdapter = [[SentryTransportAdapter alloc] initWithTransports:transports options:options]; @@ -351,6 +351,22 @@ - (SentryId *)captureCrashEvent:(SentryEvent *)event return [self sendEvent:preparedEvent withSession:session withScope:scope]; } +- (void)saveCrashTransaction:(SentryTransaction *)transaction withScope:(SentryScope *)scope +{ + SentryEvent *preparedEvent = [self prepareEvent:transaction + withScope:scope + alwaysAttachStacktrace:NO + isCrashEvent:NO]; + + if (preparedEvent == nil) { + return; + } + + SentryTraceContext *traceContext = [self getTraceStateWithEvent:transaction withScope:scope]; + + [self.transportAdapter storeEvent:preparedEvent traceContext:traceContext]; +} + - (SentryId *)captureEvent:(SentryEvent *)event { return [self captureEvent:event withScope:[[SentryScope alloc] init]]; diff --git a/Sources/Sentry/SentryCrashIntegration.m b/Sources/Sentry/SentryCrashIntegration.m index 37e5ef99c0..7c1cd46639 100644 --- a/Sources/Sentry/SentryCrashIntegration.m +++ b/Sources/Sentry/SentryCrashIntegration.m @@ -1,6 +1,7 @@ #import "SentryCrashIntegration.h" #import "SentryCrashInstallationReporter.h" +#import "SentryCrashC.h" #include "SentryCrashMonitor_Signal.h" #import "SentryCrashWrapper.h" #import "SentryDispatchQueueWrapper.h" @@ -11,6 +12,8 @@ #import "SentrySDK+Private.h" #import "SentryScope+Private.h" #import "SentrySessionCrashedHandler.h" +#import "SentrySpan+Private.h" +#import "SentryTracer.h" #import "SentryWatchdogTerminationLogic.h" #import #import @@ -34,6 +37,17 @@ static NSString *const DEVICE_KEY = @"device"; static NSString *const LOCALE_KEY = @"locale"; +void +sentry_finishAndSaveTransaction(void) +{ + SentrySpan *span = SentrySDK.currentHub.scope.span; + + if (span != nil) { + SentryTracer *tracer = [span tracer]; + [tracer finishForCrash]; + } +} + @interface SentryCrashIntegration () @property (nonatomic, weak) SentryOptions *options; @@ -110,6 +124,10 @@ - (BOOL)installWithOptions:(nonnull SentryOptions *)options [self configureScope]; + if (options.enablePersistingTracesWhenCrashing) { + [self configureTracingWhenCrashing]; + } + return YES; } @@ -193,6 +211,8 @@ - (void)uninstall installationToken = 0; } + sentrycrash_setSaveTransaction(NULL); + [NSNotificationCenter.defaultCenter removeObserver:self name:NSCurrentLocaleDidChangeNotification object:nil]; @@ -243,4 +263,9 @@ - (void)currentLocaleDidChange }]; } +- (void)configureTracingWhenCrashing +{ + sentrycrash_setSaveTransaction(&sentry_finishAndSaveTransaction); +} + @end diff --git a/Sources/Sentry/SentryCrashReportConverter.m b/Sources/Sentry/SentryCrashReportConverter.m index 2dbcc00833..8a93a966ec 100644 --- a/Sources/Sentry/SentryCrashReportConverter.m +++ b/Sources/Sentry/SentryCrashReportConverter.m @@ -112,7 +112,15 @@ - (SentryEvent *_Nullable)convertReportToEvent event.dist = self.userContext[@"dist"]; event.environment = self.userContext[@"environment"]; - event.context = self.userContext[@"context"]; + + NSMutableDictionary *mutableContext = + [[NSMutableDictionary alloc] initWithDictionary:self.userContext[@"context"]]; + if (self.userContext[@"traceContext"]) { + mutableContext[@"trace"] = self.userContext[@"traceContext"]; + } + + event.context = mutableContext; + event.extra = self.userContext[@"extra"]; event.tags = self.userContext[@"tags"]; // event.level we do not set the level here since this always resulted diff --git a/Sources/Sentry/SentryCrashScopeObserver.m b/Sources/Sentry/SentryCrashScopeObserver.m index 8059e89ac5..d6f3f7378c 100644 --- a/Sources/Sentry/SentryCrashScopeObserver.m +++ b/Sources/Sentry/SentryCrashScopeObserver.m @@ -47,6 +47,12 @@ - (void)setContext:(nullable NSDictionary *)context syncToSentryCrash:^(const void *bytes) { sentrycrash_scopesync_setContext(bytes); }]; } +- (void)setTraceContext:(nullable NSDictionary *)traceContext +{ + [self syncScope:traceContext + syncToSentryCrash:^(const void *bytes) { sentrycrash_scopesync_setTraceContext(bytes); }]; +} + - (void)setExtras:(nullable NSDictionary *)extras { [self syncScope:extras diff --git a/Sources/Sentry/SentryDependencyContainer.m b/Sources/Sentry/SentryDependencyContainer.m index c53086900d..710470a6ad 100644 --- a/Sources/Sentry/SentryDependencyContainer.m +++ b/Sources/Sentry/SentryDependencyContainer.m @@ -22,8 +22,12 @@ #import #import #import +#import #import +#import #import +#import +#import #import #import #import @@ -215,6 +219,26 @@ - (SentryNSNotificationCenterWrapper *)notificationCenterWrapper } } +- (id)rateLimits +{ + @synchronized(sentryDependencyContainerLock) { + if (_rateLimits == nil) { + SentryRetryAfterHeaderParser *retryAfterHeaderParser = + [[SentryRetryAfterHeaderParser alloc] + initWithHttpDateParser:[[SentryHttpDateParser alloc] init] + currentDateProvider:self.dateProvider]; + SentryRateLimitParser *rateLimitParser = + [[SentryRateLimitParser alloc] initWithCurrentDateProvider:self.dateProvider]; + + _rateLimits = [[SentryDefaultRateLimits alloc] + initWithRetryAfterHeaderParser:retryAfterHeaderParser + andRateLimitParser:rateLimitParser + currentDateProvider:self.dateProvider]; + } + return _rateLimits; + } +} + #if SENTRY_HAS_UIKIT - (SentryUIDeviceWrapper *)uiDeviceWrapper SENTRY_DISABLE_THREAD_SANITIZER( "double-checked lock produce false alarms") diff --git a/Sources/Sentry/SentryHttpTransport.m b/Sources/Sentry/SentryHttpTransport.m index 6ec6866380..ae71cf89dc 100644 --- a/Sources/Sentry/SentryHttpTransport.m +++ b/Sources/Sentry/SentryHttpTransport.m @@ -146,6 +146,11 @@ - (void)sendEnvelope:(SentryEnvelope *)envelope }]; } +- (void)storeEnvelope:(SentryEnvelope *)envelope +{ + [self.fileManager storeEnvelope:envelope]; +} + - (void)recordLostEvent:(SentryDataCategory)category reason:(SentryDiscardReason)reason { [self recordLostEvent:category reason:reason quantity:1]; diff --git a/Sources/Sentry/SentryHub.m b/Sources/Sentry/SentryHub.m index 79989327f5..a83f66c71c 100644 --- a/Sources/Sentry/SentryHub.m +++ b/Sources/Sentry/SentryHub.m @@ -291,6 +291,21 @@ - (void)captureTransaction:(SentryTransaction *)transaction }]; } +- (void)saveCrashTransaction:(SentryTransaction *)transaction +{ + SentrySampleDecision decision = transaction.trace.sampled; + + if (decision != kSentrySampleDecisionYes) { + // No need to update client reports when we're crashing cause they get lost anyways. + return; + } + + SentryClient *client = _client; + if (client != nil) { + [client saveCrashTransaction:transaction withScope:self.scope]; + } +} + - (SentryId *)captureEvent:(SentryEvent *)event { return [self captureEvent:event withScope:self.scope]; diff --git a/Sources/Sentry/SentryMeta.m b/Sources/Sentry/SentryMeta.m index e443f01891..6b4e0a9e7d 100644 --- a/Sources/Sentry/SentryMeta.m +++ b/Sources/Sentry/SentryMeta.m @@ -5,7 +5,7 @@ @implementation SentryMeta // Don't remove the static keyword. If you do the compiler adds the constant name to the global // symbol table and it might clash with other constants. When keeping the static keyword the // compiler replaces all occurrences with the value. -static NSString *versionString = @"8.40.0"; +static NSString *versionString = @"8.40.1"; static NSString *sdkName = @"sentry.cocoa"; + (NSString *)versionString diff --git a/Sources/Sentry/SentryOptions.m b/Sources/Sentry/SentryOptions.m index 7eb33210fc..1b91d4579b 100644 --- a/Sources/Sentry/SentryOptions.m +++ b/Sources/Sentry/SentryOptions.m @@ -108,6 +108,7 @@ - (instancetype)init self.sendDefaultPii = NO; self.enableAutoPerformanceTracing = YES; self.enablePerformanceV2 = NO; + self.enablePersistingTracesWhenCrashing = NO; self.enableCaptureFailedRequests = YES; self.environment = kSentryDefaultEnvironment; self.enableTimeToFullDisplayTracing = NO; @@ -413,6 +414,9 @@ - (BOOL)validateOptions:(NSDictionary *)options [self setBool:options[@"enablePerformanceV2"] block:^(BOOL value) { self->_enablePerformanceV2 = value; }]; + [self setBool:options[@"enablePersistingTracesWhenCrashing"] + block:^(BOOL value) { self->_enablePersistingTracesWhenCrashing = value; }]; + [self setBool:options[@"enableCaptureFailedRequests"] block:^(BOOL value) { self->_enableCaptureFailedRequests = value; }]; diff --git a/Sources/Sentry/SentryPropagationContext.h b/Sources/Sentry/SentryPropagationContext.h index 553dffb445..54b6e5bd54 100644 --- a/Sources/Sentry/SentryPropagationContext.h +++ b/Sources/Sentry/SentryPropagationContext.h @@ -8,8 +8,8 @@ NS_ASSUME_NONNULL_BEGIN @interface SentryPropagationContext : NSObject -@property (nonatomic, strong) SentryId *traceId; -@property (nonatomic, strong) SentrySpanId *spanId; +@property (nonatomic, strong, readonly) SentryId *traceId; +@property (nonatomic, strong, readonly) SentrySpanId *spanId; @property (nonatomic, readonly) SentryTraceHeader *traceHeader; - (NSDictionary *)traceContextForEvent; diff --git a/Sources/Sentry/SentryPropagationContext.m b/Sources/Sentry/SentryPropagationContext.m index 965f3ab19b..ac56aa1fad 100644 --- a/Sources/Sentry/SentryPropagationContext.m +++ b/Sources/Sentry/SentryPropagationContext.m @@ -8,8 +8,8 @@ @implementation SentryPropagationContext - (instancetype)init { if (self = [super init]) { - self.traceId = [[SentryId alloc] init]; - self.spanId = [[SentrySpanId alloc] init]; + _traceId = [[SentryId alloc] init]; + _spanId = [[SentrySpanId alloc] init]; } return self; } diff --git a/Sources/Sentry/SentryScope.m b/Sources/Sentry/SentryScope.m index 21fb2e873b..354bc3b6a6 100644 --- a/Sources/Sentry/SentryScope.m +++ b/Sources/Sentry/SentryScope.m @@ -97,7 +97,7 @@ - (instancetype)initWithScope:(SentryScope *)scope [_fingerprintArray addObjectsFromArray:[scope fingerprints]]; [_attachmentArray addObjectsFromArray:[scope attachments]]; - self.propagationContext = [[SentryPropagationContext alloc] init]; + self.propagationContext = scope.propagationContext; self.maxBreadcrumbs = scope.maxBreadcrumbs; self.userObject = scope.userObject.copy; self.distString = scope.distString; @@ -143,6 +143,10 @@ - (void)setSpan:(nullable id)span { @synchronized(_spanLock) { _span = span; + + for (id observer in self.observers) { + [observer setTraceContext:[self buildTraceContext:span]]; + } } } @@ -453,9 +457,18 @@ - (void)clearAttachments if (self.extras.count > 0) { [serializedData setValue:[self extras] forKey:@"extra"]; } - if (self.context.count > 0) { - [serializedData setValue:[self context] forKey:@"context"]; + + NSDictionary *traceContext = nil; + @synchronized(_spanLock) { + traceContext = [self buildTraceContext:_span]; + } + serializedData[@"traceContext"] = traceContext; + + NSDictionary *context = [self context]; + if (context.count > 0) { + [serializedData setValue:context forKey:@"context"]; } + [serializedData setValue:[self.userObject serialize] forKey:@"user"]; [serializedData setValue:self.distString forKey:@"dist"]; [serializedData setValue:self.environmentString forKey:@"environment"]; @@ -571,8 +584,9 @@ - (SentryEvent *__nullable)applyToEvent:(SentryEvent *)event return event; } + id span; + if (self.span != nil) { - id span; @synchronized(_spanLock) { span = self.span; } @@ -583,14 +597,10 @@ - (SentryEvent *__nullable)applyToEvent:(SentryEvent *)event [span isKindOfClass:[SentryTracer class]]) { event.transaction = [[(SentryTracer *)span transactionContext] name]; } - newContext[@"trace"] = [span serialize]; } } - if (newContext[@"trace"] == nil) { - // If this is an error event we need to add the distributed trace context. - newContext[@"trace"] = [self.propagationContext traceContextForEvent]; - } + newContext[@"trace"] = [self buildTraceContext:span]; event.context = newContext; return event; @@ -601,6 +611,18 @@ - (void)addObserver:(id)observer [self.observers addObject:observer]; } +/** + * Make sure to call this inside @c synchronized(_spanLock) caus this method isn't thread safe. + */ +- (NSDictionary *)buildTraceContext:(nullable id)span +{ + if (span != nil) { + return [span serialize]; + } else { + return [self.propagationContext traceContextForEvent]; + } +} + @end NS_ASSUME_NONNULL_END diff --git a/Sources/Sentry/SentryScopeSyncC.c b/Sources/Sentry/SentryScopeSyncC.c index cbbacd85fc..4d518f3cf4 100644 --- a/Sources/Sentry/SentryScopeSyncC.c +++ b/Sources/Sentry/SentryScopeSyncC.c @@ -46,6 +46,12 @@ sentrycrash_scopesync_setContext(const char *const jsonEncodedCString) setField(jsonEncodedCString, &scope.context); } +void +sentrycrash_scopesync_setTraceContext(const char *const jsonEncodedCString) +{ + setField(jsonEncodedCString, &scope.traceContext); +} + void sentrycrash_scopesync_setEnvironment(const char *const jsonEncodedCString) { diff --git a/Sources/Sentry/SentrySessionReplayIntegration.m b/Sources/Sentry/SentrySessionReplayIntegration.m index f24a3abf27..85c1dd746a 100644 --- a/Sources/Sentry/SentrySessionReplayIntegration.m +++ b/Sources/Sentry/SentrySessionReplayIntegration.m @@ -14,6 +14,7 @@ # import "SentryNSNotificationCenterWrapper.h" # import "SentryOptions.h" # import "SentryRandom.h" +# import "SentryRateLimits.h" # import "SentryReachability.h" # import "SentrySDK+Private.h" # import "SentryScope+Private.h" @@ -46,6 +47,12 @@ @implementation SentrySessionReplayIntegration { SentryReplayOptions *_replayOptions; SentryNSNotificationCenterWrapper *_notificationCenter; SentryOnDemandReplay *_resumeReplayMaker; + id _rateLimits; + // We need to use this variable to identify whether rate limiting was ever activated for session + // replay in this session, instead of always looking for the rate status in `SentryRateLimits` + // This is the easiest way to ensure segment 0 will always reach the server, because session + // replay absolutely needs segment 0 to make replay work. + BOOL _rateLimited; } - (instancetype)init @@ -78,6 +85,7 @@ - (void)setupWith:(SentryReplayOptions *)replayOptions enableTouchTracker:(BOOL) { _replayOptions = replayOptions; _viewPhotographer = [[SentryViewPhotographer alloc] initWithRedactOptions:replayOptions]; + _rateLimits = SentryDependencyContainer.sharedInstance.rateLimits; if (touchTracker) { _touchTracker = [[SentryTouchTracker alloc] @@ -416,6 +424,12 @@ - (void)resume - (void)start { + if (_rateLimited) { + SENTRY_LOG_WARN( + @"This session was rate limited. Not starting session replay until next app session"); + return; + } + if (self.sessionReplay != nil) { if (self.sessionReplay.isFullSession == NO) { [self.sessionReplay captureReplay]; @@ -447,6 +461,7 @@ - (void)sentrySessionEnded:(SentrySession *)session - (void)sentrySessionStarted:(SentrySession *)session { + _rateLimited = NO; [self startSession]; } @@ -553,6 +568,15 @@ - (void)sessionReplayNewSegmentWithReplayEvent:(SentryReplayEvent *)replayEvent replayRecording:(SentryReplayRecording *)replayRecording videoUrl:(NSURL *)videoUrl { + if ([_rateLimits isRateLimitActive:kSentryDataCategoryReplay] || + [_rateLimits isRateLimitActive:kSentryDataCategoryAll]) { + SENTRY_LOG_DEBUG( + @"Rate limiting is active for replays. Stopping session replay until next session."); + _rateLimited = YES; + [self stop]; + return; + } + [SentrySDK.currentHub captureReplayEvent:replayEvent replayRecording:replayRecording video:videoUrl]; diff --git a/Sources/Sentry/SentrySpotlightTransport.m b/Sources/Sentry/SentrySpotlightTransport.m index f268a1cb6f..55806fc04b 100644 --- a/Sources/Sentry/SentrySpotlightTransport.m +++ b/Sources/Sentry/SentrySpotlightTransport.m @@ -86,6 +86,11 @@ - (void)sendEnvelope:(SentryEnvelope *)envelope }]; } +- (void)storeEnvelope:(SentryEnvelope *)envelope +{ + [self sendEnvelope:envelope]; +} + - (SentryFlushResult)flush:(NSTimeInterval)timeout { // Empty on purpose diff --git a/Sources/Sentry/SentryTracer.m b/Sources/Sentry/SentryTracer.m index f5d41be75f..d3d23364f4 100644 --- a/Sources/Sentry/SentryTracer.m +++ b/Sources/Sentry/SentryTracer.m @@ -467,6 +467,23 @@ - (void)finishWithStatus:(SentrySpanStatus)status [self canBeFinished]; } +- (void)finishForCrash +{ + self.wasFinishCalled = YES; + _finishStatus = kSentrySpanStatusInternalError; + + // We don't need to clean up during finish cause we're crashing, and the cleanup can execute + // code that leads to the app hanging and not terminating. + BOOL discardTransaction = [self finishTracer:kSentrySpanStatusInternalError shouldCleanUp:NO]; + if (discardTransaction) { + return; + } + + SentryTransaction *transaction = [self toTransaction]; + + [_hub saveCrashTransaction:transaction]; +} + - (void)canBeFinished { // Transaction already finished and captured. @@ -521,16 +538,57 @@ - (BOOL)hasUnfinishedChildSpansToWaitFor - (void)finishInternal { - [self cancelDeadlineTimer]; + BOOL discardTransaction = [self finishTracer:kSentrySpanStatusDeadlineExceeded + shouldCleanUp:YES]; + if (discardTransaction) { + return; + } + + SentryTransaction *transaction = [self toTransaction]; + +#if SENTRY_TARGET_PROFILING_SUPPORTED + if (self.isProfiling) { + NSDate *startTimestamp; + +# if SENTRY_HAS_UIKIT + if (appStartMeasurement != nil) { + startTimestamp = appStartMeasurement.runtimeInitTimestamp; + } +# endif // SENTRY_HAS_UIKIT + + if (startTimestamp == nil) { + startTimestamp = self.startTimestamp; + } + if (!SENTRY_ASSERT_RETURN(startTimestamp != nil, + @"A transaction with a profile should have a start timestamp already. We will " + @"assign the current time but this will be incorrect.")) { + startTimestamp = [SentryDependencyContainer.sharedInstance.dateProvider date]; + } + + sentry_captureTransactionWithProfile( + self.hub, self.dispatchQueue, transaction, startTimestamp); + return; + } +#endif // SENTRY_TARGET_PROFILING_SUPPORTED + + [_hub captureTransaction:transaction withScope:_hub.scope]; +} + +- (BOOL)finishTracer:(SentrySpanStatus)unfinishedSpansFinishStatus shouldCleanUp:(BOOL)shouldCleanUp +{ + if (shouldCleanUp) { + [self cancelDeadlineTimer]; + } + if (self.isFinished) { SENTRY_LOG_DEBUG(@"Tracer %@ was already finished.", _traceContext.traceId.sentryIdString); - return; + return YES; } @synchronized(self) { if (self.isFinished) { SENTRY_LOG_DEBUG(@"Tracer %@ was already finished after synchronizing.", _traceContext.traceId.sentryIdString); - return; + return YES; } // Keep existing status of auto generated transactions if set by the user. @@ -548,14 +606,16 @@ - (void)finishInternal } #endif // SENTRY_HAS_UIKIT - [self.delegate tracerDidFinish:self]; + if (shouldCleanUp) { + [self.delegate tracerDidFinish:self]; - if (self.finishCallback) { - self.finishCallback(self); + if (self.finishCallback) { + self.finishCallback(self); - // The callback will only be executed once. No need to keep the reference and we avoid - // potential retain cycles. - self.finishCallback = nil; + // The callback will only be executed once. No need to keep the reference and we avoid + // potential retain cycles. + self.finishCallback = nil; + } } // Prewarming can execute code up to viewDidLoad of a UIViewController, and keep the app in the @@ -567,37 +627,39 @@ - (void)finishInternal SENTRY_LOG_INFO(@"Auto generated transaction exceeded the max duration of %f seconds. Not " @"capturing transaction.", SENTRY_AUTO_TRANSACTION_MAX_DURATION); - return; + return YES; } if (_hub == nil) { SENTRY_LOG_DEBUG( @"Hub was nil for tracer %@, nothing to do.", _traceContext.traceId.sentryIdString); - return; + return YES; } - [_hub.scope useSpan:^(id _Nullable span) { - if (span == self) { - [self->_hub.scope setSpan:nil]; - } - }]; + if (shouldCleanUp) { + [_hub.scope useSpan:^(id _Nullable span) { + if (span == self) { + [self->_hub.scope setSpan:nil]; + } + }]; + } if (self.configuration.finishMustBeCalled && !self.wasFinishCalled) { SENTRY_LOG_DEBUG( @"Not capturing transaction because finish was not called before timing out."); - return; + return YES; } @synchronized(_children) { if (_configuration.idleTimeout > 0.0 && _children.count == 0) { SENTRY_LOG_DEBUG(@"Was waiting for timeout for UI event trace but it had no children, " @"will not keep transaction."); - return; + return YES; } for (id span in _children) { if (!span.isFinished) { - [span finishWithStatus:kSentrySpanStatusDeadlineExceeded]; + [span finishWithStatus:unfinishedSpansFinishStatus]; // Unfinished children should have the same // end timestamp as their parent transaction @@ -610,34 +672,7 @@ - (void)finishInternal } } - SentryTransaction *transaction = [self toTransaction]; - -#if SENTRY_TARGET_PROFILING_SUPPORTED - if (self.isProfiling) { - NSDate *startTimestamp; - -# if SENTRY_HAS_UIKIT - if (appStartMeasurement != nil) { - startTimestamp = appStartMeasurement.runtimeInitTimestamp; - } -# endif // SENTRY_HAS_UIKIT - - if (startTimestamp == nil) { - startTimestamp = self.startTimestamp; - } - if (!SENTRY_ASSERT_RETURN(startTimestamp != nil, - @"A transaction with a profile should have a start timestamp already. We will " - @"assign the current time but this will be incorrect.")) { - startTimestamp = [SentryDependencyContainer.sharedInstance.dateProvider date]; - } - - sentry_captureTransactionWithProfile( - self.hub, self.dispatchQueue, transaction, startTimestamp); - return; - } -#endif // SENTRY_TARGET_PROFILING_SUPPORTED - - [_hub captureTransaction:transaction withScope:_hub.scope]; + return NO; } - (void)trimEndTimestamp diff --git a/Sources/Sentry/SentryTransportAdapter.m b/Sources/Sentry/SentryTransportAdapter.m index b63453bd53..0a1027430b 100644 --- a/Sources/Sentry/SentryTransportAdapter.m +++ b/Sources/Sentry/SentryTransportAdapter.m @@ -77,6 +77,21 @@ - (void)sendEvent:(SentryEvent *)event [self sendEnvelope:envelope]; } +- (void)storeEvent:(SentryEvent *)event traceContext:(nullable SentryTraceContext *)traceContext +{ + SentryEnvelopeItem *item = [[SentryEnvelopeItem alloc] initWithEvent:event]; + + SentryEnvelopeHeader *envelopeHeader = [[SentryEnvelopeHeader alloc] initWithId:event.eventId + traceContext:traceContext]; + + SentryEnvelope *envelope = [[SentryEnvelope alloc] initWithHeader:envelopeHeader + items:@[ item ]]; + + for (id transport in self.transports) { + [transport storeEnvelope:envelope]; + } +} + - (void)sendUserFeedback:(SentryUserFeedback *)userFeedback { SentryEnvelopeItem *item = [[SentryEnvelopeItem alloc] initWithUserFeedback:userFeedback]; diff --git a/Sources/Sentry/SentryTransportFactory.m b/Sources/Sentry/SentryTransportFactory.m index cbdd10e599..e689706917 100644 --- a/Sources/Sentry/SentryTransportFactory.m +++ b/Sources/Sentry/SentryTransportFactory.m @@ -25,7 +25,7 @@ @implementation SentryTransportFactory + (NSArray> *)initTransports:(SentryOptions *)options sentryFileManager:(SentryFileManager *)sentryFileManager - currentDateProvider:(id)currentDateProvider + rateLimits:(id)rateLimits { NSURLSession *session; @@ -42,17 +42,6 @@ @implementation SentryTransportFactory id requestManager = [[SentryQueueableRequestManager alloc] initWithSession:session]; - SentryHttpDateParser *httpDateParser = [[SentryHttpDateParser alloc] init]; - SentryRetryAfterHeaderParser *retryAfterHeaderParser = - [[SentryRetryAfterHeaderParser alloc] initWithHttpDateParser:httpDateParser - currentDateProvider:currentDateProvider]; - SentryRateLimitParser *rateLimitParser = - [[SentryRateLimitParser alloc] initWithCurrentDateProvider:currentDateProvider]; - id rateLimits = - [[SentryDefaultRateLimits alloc] initWithRetryAfterHeaderParser:retryAfterHeaderParser - andRateLimitParser:rateLimitParser - currentDateProvider:currentDateProvider]; - SentryEnvelopeRateLimit *envelopeRateLimit = [[SentryEnvelopeRateLimit alloc] initWithRateLimits:rateLimits]; diff --git a/Sources/Sentry/SentryWatchdogTerminationScopeObserver.m b/Sources/Sentry/SentryWatchdogTerminationScopeObserver.m index 635160b46f..9dc47a9e6f 100644 --- a/Sources/Sentry/SentryWatchdogTerminationScopeObserver.m +++ b/Sources/Sentry/SentryWatchdogTerminationScopeObserver.m @@ -166,6 +166,11 @@ - (void)setUser:(nullable SentryUser *)user // Left blank on purpose } +- (void)setTraceContext:(nullable NSDictionary *)traceContext +{ + // Left blank on purpose +} + @end #endif // SENTRY_HAS_UIKIT diff --git a/Sources/Sentry/include/HybridPublic/SentryDependencyContainer.h b/Sources/Sentry/include/HybridPublic/SentryDependencyContainer.h index 1a8491bc43..433cace15e 100644 --- a/Sources/Sentry/include/HybridPublic/SentryDependencyContainer.h +++ b/Sources/Sentry/include/HybridPublic/SentryDependencyContainer.h @@ -24,6 +24,7 @@ @class SentryThreadInspector; @protocol SentryRandom; @protocol SentryCurrentDateProvider; +@protocol SentryRateLimits; #if SENTRY_HAS_METRIC_KIT @class SentryMXManager; @@ -75,6 +76,7 @@ SENTRY_NO_INIT @property (nonatomic, strong) SentryExtraContextProvider *extraContextProvider; @property (nonatomic, strong) SentrySysctl *sysctlWrapper; @property (nonatomic, strong) SentryThreadInspector *threadInspector; +@property (nonatomic, strong) id rateLimits; #if SENTRY_UIKIT_AVAILABLE @property (nonatomic, strong) SentryFramesTracker *framesTracker; diff --git a/Sources/Sentry/include/SentryClient+Private.h b/Sources/Sentry/include/SentryClient+Private.h index ccd95f1471..e554b751f8 100644 --- a/Sources/Sentry/include/SentryClient+Private.h +++ b/Sources/Sentry/include/SentryClient+Private.h @@ -36,6 +36,10 @@ NS_ASSUME_NONNULL_BEGIN withSession:(SentrySession *)session withScope:(SentryScope *)scope; +- (void)saveCrashTransaction:(SentryTransaction *)transaction + withScope:(SentryScope *)scope + NS_SWIFT_NAME(saveCrashTransaction(transaction:scope:)); + - (SentryId *)captureEvent:(SentryEvent *)event withScope:(SentryScope *)scope additionalEnvelopeItems:(NSArray *)additionalEnvelopeItems diff --git a/Sources/Sentry/include/SentryHub+Private.h b/Sources/Sentry/include/SentryHub+Private.h index a99c24a87f..531ec23b6d 100644 --- a/Sources/Sentry/include/SentryHub+Private.h +++ b/Sources/Sentry/include/SentryHub+Private.h @@ -59,6 +59,7 @@ NS_ASSUME_NONNULL_BEGIN - (void)captureTransaction:(SentryTransaction *)transaction withScope:(SentryScope *)scope additionalEnvelopeItems:(NSArray *)additionalEnvelopeItems; +- (void)saveCrashTransaction:(SentryTransaction *)transaction; - (void)storeEnvelope:(SentryEnvelope *)envelope; - (void)captureEnvelope:(SentryEnvelope *)envelope; diff --git a/Sources/Sentry/include/SentryScopeObserver.h b/Sources/Sentry/include/SentryScopeObserver.h index f63c4c0783..98894f86f6 100644 --- a/Sources/Sentry/include/SentryScopeObserver.h +++ b/Sources/Sentry/include/SentryScopeObserver.h @@ -17,6 +17,8 @@ NS_ASSUME_NONNULL_BEGIN - (void)setContext:(nullable NSDictionary *)context; +- (void)setTraceContext:(nullable NSDictionary *)traceContext; + - (void)setDist:(nullable NSString *)dist; - (void)setEnvironment:(nullable NSString *)environment; diff --git a/Sources/Sentry/include/SentryScopeSyncC.h b/Sources/Sentry/include/SentryScopeSyncC.h index 98437f555c..9ee63ba9b2 100644 --- a/Sources/Sentry/include/SentryScopeSyncC.h +++ b/Sources/Sentry/include/SentryScopeSyncC.h @@ -5,6 +5,7 @@ typedef struct { char *user; char *dist; char *context; + char *traceContext; char *environment; char *tags; char *extras; @@ -13,6 +14,7 @@ typedef struct { char **breadcrumbs; // dynamic array of char arrays long maxCrumbs; long currentCrumb; + } SentryCrashScope; SentryCrashScope *sentrycrash_scopesync_getScope(void); @@ -29,6 +31,8 @@ void sentrycrash_scopesync_setDist(const char *const jsonEncodedCString); void sentrycrash_scopesync_setContext(const char *const jsonEncodedCString); +void sentrycrash_scopesync_setTraceContext(const char *const jsonEncodedCString); + void sentrycrash_scopesync_setEnvironment(const char *const jsonEncodedCString); void sentrycrash_scopesync_setTags(const char *const jsonEncodedCString); diff --git a/Sources/Sentry/include/SentryTracer.h b/Sources/Sentry/include/SentryTracer.h index 33dfd76ccb..491c49af17 100644 --- a/Sources/Sentry/include/SentryTracer.h +++ b/Sources/Sentry/include/SentryTracer.h @@ -93,6 +93,13 @@ static const NSTimeInterval SENTRY_AUTO_TRANSACTION_MAX_DURATION = 500.0; - (void)dispatchIdleTimeout; +/** + * This method is designed to be used when the app crashes. It finishes the transaction and stores + * it to disk on the calling thread. This method skips adding a profile to the transaction to + * increase the likelihood of storing it before the app exits. + */ +- (void)finishForCrash; + @end NS_ASSUME_NONNULL_END diff --git a/Sources/Sentry/include/SentryTransport.h b/Sources/Sentry/include/SentryTransport.h index c3cdcd1bd0..6fdb5e4786 100644 --- a/Sources/Sentry/include/SentryTransport.h +++ b/Sources/Sentry/include/SentryTransport.h @@ -17,6 +17,8 @@ NS_SWIFT_NAME(Transport) - (void)sendEnvelope:(SentryEnvelope *)envelope NS_SWIFT_NAME(send(envelope:)); +- (void)storeEnvelope:(SentryEnvelope *)envelope; + - (void)recordLostEvent:(SentryDataCategory)category reason:(SentryDiscardReason)reason; - (void)recordLostEvent:(SentryDataCategory)category diff --git a/Sources/Sentry/include/SentryTransportAdapter.h b/Sources/Sentry/include/SentryTransportAdapter.h index 81c7f36fc4..2182eb630f 100644 --- a/Sources/Sentry/include/SentryTransportAdapter.h +++ b/Sources/Sentry/include/SentryTransportAdapter.h @@ -39,6 +39,8 @@ SENTRY_NO_INIT traceContext:(nullable SentryTraceContext *)traceContext attachments:(NSArray *)attachments; +- (void)storeEvent:(SentryEvent *)event traceContext:(nullable SentryTraceContext *)traceContext; + - (void)sendUserFeedback:(SentryUserFeedback *)userFeedback NS_SWIFT_NAME(send(userFeedback:)); - (void)sendEnvelope:(SentryEnvelope *)envelope NS_SWIFT_NAME(send(envelope:)); diff --git a/Sources/Sentry/include/SentryTransportFactory.h b/Sources/Sentry/include/SentryTransportFactory.h index da079af079..7568630803 100644 --- a/Sources/Sentry/include/SentryTransportFactory.h +++ b/Sources/Sentry/include/SentryTransportFactory.h @@ -4,6 +4,7 @@ @class SentryOptions, SentryFileManager; @protocol SentryCurrentDateProvider; +@protocol SentryRateLimits; NS_ASSUME_NONNULL_BEGIN @@ -12,7 +13,7 @@ NS_SWIFT_NAME(TransportInitializer) + (NSArray> *)initTransports:(SentryOptions *)options sentryFileManager:(SentryFileManager *)sentryFileManager - currentDateProvider:(id)currentDateProvider; + rateLimits:(id)rateLimits; @end diff --git a/Sources/SentryCrash/Recording/SentryCrashC.c b/Sources/SentryCrash/Recording/SentryCrashC.c index 1225728bf6..3944bb4473 100644 --- a/Sources/SentryCrash/Recording/SentryCrashC.c +++ b/Sources/SentryCrash/Recording/SentryCrashC.c @@ -58,7 +58,7 @@ static SentryCrashMonitorType g_monitoring = SentryCrashMonitorTypeProductionSaf static char g_lastCrashReportFilePath[SentryCrashFU_MAX_PATH_LENGTH]; static void (*g_saveScreenShot)(const char *) = 0; static void (*g_saveViewHierarchy)(const char *) = 0; - +static void (*g_saveTransaction)(void) = 0; // ============================================================================ #pragma mark - Utility - // ============================================================================ @@ -88,7 +88,8 @@ onCrash(struct SentryCrash_MonitorContext *monitorContext) } // Report is saved to disk, now we try to take screenshots - // and view hierarchies. + // and view hierarchies, and try to save any ongoing transaction + // bound to the scope. // Depending on the state of the crash this may not work // because we gonna call into non async-signal safe code // but since the app is already in a crash state we don't @@ -108,6 +109,10 @@ onCrash(struct SentryCrash_MonitorContext *monitorContext) } } } + + if (g_saveTransaction) { + g_saveTransaction(); + } } // ============================================================================ @@ -201,6 +206,12 @@ sentrycrash_setSaveViewHierarchy(void (*callback)(const char *)) g_saveViewHierarchy = callback; } +void +sentrycrash_setSaveTransaction(void (*callback)(void)) +{ + g_saveTransaction = callback; +} + void sentrycrash_notifyAppActive(bool isActive) { @@ -289,3 +300,17 @@ sentrycrash_hasSaveViewHierarchyCallback(void) { return g_saveViewHierarchy != NULL; } + +bool +sentrycrash_hasSaveTransaction(void) +{ + return g_saveTransaction != NULL; +} + +void +sentrycrash_invokeSaveTransaction(void) +{ + if (g_saveTransaction) { + g_saveTransaction(); + } +} diff --git a/Sources/SentryCrash/Recording/SentryCrashC.h b/Sources/SentryCrash/Recording/SentryCrashC.h index 2c9fcd1b04..45654a2c99 100644 --- a/Sources/SentryCrash/Recording/SentryCrashC.h +++ b/Sources/SentryCrash/Recording/SentryCrashC.h @@ -139,6 +139,12 @@ void sentrycrash_setSaveScreenshots(SaveAttachmentCallback callback); */ void sentrycrash_setSaveViewHierarchy(SaveAttachmentCallback callback); +/** + * Set the callback to be called at the end of a crash to make the app save the ongoing transaction + * bound to the scope. + */ +void sentrycrash_setSaveTransaction(void (*callback)(void)); + #pragma mark-- Notifications -- /** Notify the crash reporter of the application active state. @@ -218,6 +224,17 @@ bool sentrycrash_hasSaveScreenshotCallback(void); */ bool sentrycrash_hasSaveViewHierarchyCallback(void); +/** + * For testing purpose. + * Indicates that a callback was registered for saving the transaction. + */ +bool sentrycrash_hasSaveTransaction(void); + +/** + * For testing purposes. + */ +void sentrycrash_invokeSaveTransaction(void); + #ifdef __cplusplus } #endif diff --git a/Sources/SentryCrash/Recording/SentryCrashReport.c b/Sources/SentryCrash/Recording/SentryCrashReport.c index 39ce682c74..7e842ed914 100644 --- a/Sources/SentryCrash/Recording/SentryCrashReport.c +++ b/Sources/SentryCrash/Recording/SentryCrashReport.c @@ -1612,6 +1612,9 @@ writeScopeJson(const SentryCrashReportWriter *const writer) if (scope->context) { addJSONElement(writer, "context", scope->context, false); } + if (scope->traceContext) { + addJSONElement(writer, "traceContext", scope->traceContext, false); + } if (scope->environment) { addJSONElement(writer, "environment", scope->environment, false); } diff --git a/Sources/Swift/Tools/UIRedactBuilder.swift b/Sources/Swift/Tools/UIRedactBuilder.swift index 5e1bd3f985..45635df3a9 100644 --- a/Sources/Swift/Tools/UIRedactBuilder.swift +++ b/Sources/Swift/Tools/UIRedactBuilder.swift @@ -158,9 +158,9 @@ class UIRedactBuilder { var redactingRegions = [RedactRegion]() self.mapRedactRegion(fromView: view, + relativeTo: nil, redacting: &redactingRegions, - rootFrame: view.frame, - transform: CGAffineTransform.identity) + rootFrame: view.frame) var swiftUIRedact = [RedactRegion]() var otherRegions = [RedactRegion]() @@ -198,12 +198,12 @@ class UIRedactBuilder { return image.imageAsset?.value(forKey: "_containingBundle") == nil } - private func mapRedactRegion(fromView view: UIView, redacting: inout [RedactRegion], rootFrame: CGRect, transform: CGAffineTransform, forceRedact: Bool = false) { + private func mapRedactRegion(fromView view: UIView, relativeTo parentLayer: CALayer?, redacting: inout [RedactRegion], rootFrame: CGRect, forceRedact: Bool = false) { guard !redactClassesIdentifiers.isEmpty && !view.isHidden && view.alpha != 0 else { return } let layer = view.layer.presentation() ?? view.layer - let newTransform = concatenateTranform(transform, with: layer) + let newTransform = getTranform(from: layer, withParent: parentLayer) let ignore = !forceRedact && shouldIgnore(view: view) let swiftUI = SentryRedactViewHelper.shouldRedactSwiftUI(view) @@ -233,23 +233,24 @@ class UIRedactBuilder { redacting.append(RedactRegion(size: layer.bounds.size, transform: newTransform, type: .clipEnd)) } for subview in view.subviews.sorted(by: { $0.layer.zPosition < $1.layer.zPosition }) { - mapRedactRegion(fromView: subview, redacting: &redacting, rootFrame: rootFrame, transform: newTransform, forceRedact: enforceRedact) + mapRedactRegion(fromView: subview, relativeTo: layer, redacting: &redacting, rootFrame: rootFrame, forceRedact: enforceRedact) } if view.clipsToBounds { redacting.append(RedactRegion(size: layer.bounds.size, transform: newTransform, type: .clipBegin)) } } - + /** - Apply the layer transformation and position to given transformation. + Gets a transform that represents the layer global position. */ - private func concatenateTranform(_ transform: CGAffineTransform, with layer: CALayer) -> CGAffineTransform { + private func getTranform(from layer: CALayer, withParent parentLayer: CALayer?) -> CGAffineTransform { let size = layer.bounds.size - let layerMiddle = CGPoint(x: size.width * layer.anchorPoint.x, y: size.height * layer.anchorPoint.y) - - var newTransform = transform.translatedBy(x: layer.position.x, y: layer.position.y) + let anchorPoint = CGPoint(x: size.width * layer.anchorPoint.x, y: size.height * layer.anchorPoint.y) + let position = parentLayer?.convert(layer.position, to: nil) ?? layer.position + + var newTransform = CGAffineTransform(translationX: position.x, y: position.y) newTransform = CATransform3DGetAffineTransform(layer.transform).concatenating(newTransform) - return newTransform.translatedBy(x: -layerMiddle.x, y: -layerMiddle.y) + return newTransform.translatedBy(x: -anchorPoint.x, y: -anchorPoint.y) } /** diff --git a/Tests/HybridSDKTest/HybridPod.podspec b/Tests/HybridSDKTest/HybridPod.podspec index 01ddd5c244..3a161883ca 100644 --- a/Tests/HybridSDKTest/HybridPod.podspec +++ b/Tests/HybridSDKTest/HybridPod.podspec @@ -13,6 +13,6 @@ Pod::Spec.new do |s| s.requires_arc = true s.frameworks = 'Foundation' s.swift_versions = "5.5" - s.dependency "Sentry/HybridSDK", "8.40.0" + s.dependency "Sentry/HybridSDK", "8.40.1" s.source_files = "HybridTest.swift" end diff --git a/Tests/SentryTests/Integrations/SentryCrash/SentryCrashIntegrationTests.swift b/Tests/SentryTests/Integrations/SentryCrash/SentryCrashIntegrationTests.swift index f38bb3fd53..b3b4cf4142 100644 --- a/Tests/SentryTests/Integrations/SentryCrash/SentryCrashIntegrationTests.swift +++ b/Tests/SentryTests/Integrations/SentryCrash/SentryCrashIntegrationTests.swift @@ -22,6 +22,7 @@ class SentryCrashIntegrationTests: NotificationCenterTestCase { options = Options() options.dsn = SentryCrashIntegrationTests.dsnAsString options.releaseName = TestData.appState.releaseName + options.tracesSampleRate = 1.0 client = TestClient(options: options, fileManager: try! SentryFileManager(options: options, dispatchQueueWrapper: dispatchQueueWrapper), deleteOldEnvelopeItems: false) hub = TestHub(client: client, andScope: nil) @@ -336,6 +337,106 @@ class SentryCrashIntegrationTests: NotificationCenterTestCase { } #endif // os(macOS) + func testEnableTracingForCrashes_SetsCallback() throws { + let (sut, _) = givenSutWithGlobalHubAndCrashWrapper() + let options = Options() + options.enablePersistingTracesWhenCrashing = true + sut.install(with: options) + + XCTAssertTrue(sentrycrash_hasSaveTransaction()) + } + + func testEnableTracingForCrashes_Uninstall_RemovesCallback() throws { + let (sut, _) = givenSutWithGlobalHubAndCrashWrapper() + let options = Options() + options.enablePersistingTracesWhenCrashing = true + sut.install(with: options) + + sut.uninstall() + + XCTAssertFalse(sentrycrash_hasSaveTransaction()) + } + + func testEnableTracingForCrashes_Disabled_DoesNotSetCallback() throws { + let (sut, _) = givenSutWithGlobalHubAndCrashWrapper() + let options = Options() + options.enablePersistingTracesWhenCrashing = false + sut.install(with: options) + + XCTAssertFalse(sentrycrash_hasSaveTransaction()) + } + + func testEnableTracingForCrashes_InvokeCallback_StoresTransaction() throws { + let options = fixture.options + options.enablePersistingTracesWhenCrashing = true + + let client = SentryClient(options: options) + defer { client?.fileManager.deleteAllEnvelopes() } + let hub = SentryHub(client: client, andScope: nil) + SentrySDK.setCurrentHub(hub) + + let sut = fixture.getSut(crashWrapper: SentryCrashWrapper.sharedInstance()) + sut.install(with: options) + + let transaction = SentrySDK.startTransaction(name: "Crashing", operation: "Operation", bindToScope: true) + + sentrycrash_invokeSaveTransaction() + + XCTAssertTrue(transaction.isFinished) + + XCTAssertEqual(1, client?.fileManager.getAllEnvelopes().count) + let transactionEnvelopeFileContents = try XCTUnwrap(client?.fileManager.getOldestEnvelope()) + let envelope = try XCTUnwrap(SentrySerialization.envelope(with: transactionEnvelopeFileContents.contents)) + XCTAssertEqual(1, envelope.items.count) + XCTAssertEqual("transaction", envelope.items.first?.header.type) + } + + func testEnableTracingForCrashes_InvokeCallbackWhenNoSpanOnScope_TransactionNotFinished() throws { + let options = fixture.options + options.enablePersistingTracesWhenCrashing = true + + let client = SentryClient(options: options) + defer { client?.fileManager.deleteAllEnvelopes() } + let hub = SentryHub(client: client, andScope: nil) + SentrySDK.setCurrentHub(hub) + + let sut = fixture.getSut(crashWrapper: SentryCrashWrapper.sharedInstance()) + sut.install(with: options) + + let transaction = SentrySDK.startTransaction(name: "name", operation: "operation", bindToScope: true) + SentrySDK.currentHub().scope.span = nil + + sentrycrash_invokeSaveTransaction() + + XCTAssertFalse(transaction.isFinished) + XCTAssertEqual(0, client?.fileManager.getAllEnvelopes().count) + } + + func testEnableTracingForCrashes_InvokeCallback_WhenSpanOnScopeIsNotATracer_StoresTransaction() throws { + let options = fixture.options + options.enablePersistingTracesWhenCrashing = true + + let client = SentryClient(options: options) + defer { client?.fileManager.deleteAllEnvelopes() } + let hub = SentryHub(client: client, andScope: nil) + SentrySDK.setCurrentHub(hub) + + let sut = fixture.getSut(crashWrapper: SentryCrashWrapper.sharedInstance()) + sut.install(with: options) + + let transaction = SentrySDK.startTransaction(name: "name", operation: "operation", bindToScope: true) + let span = transaction.startChild(operation: "child") + SentrySDK.currentHub().scope.span = span + + sentrycrash_invokeSaveTransaction() + + XCTAssertEqual(1, client?.fileManager.getAllEnvelopes().count) + let transactionEnvelopeFileContents = try XCTUnwrap(client?.fileManager.getOldestEnvelope()) + let envelope = try XCTUnwrap(SentrySerialization.envelope(with: transactionEnvelopeFileContents.contents)) + XCTAssertEqual(1, envelope.items.count) + XCTAssertEqual("transaction", envelope.items.first?.header.type) + } + private func givenCurrentSession() -> SentrySession { // serialize sets the timestamp let session = SentrySession(jsonObject: fixture.session.serialize())! diff --git a/Tests/SentryTests/Integrations/SentryCrash/SentryCrashReportTests.swift b/Tests/SentryTests/Integrations/SentryCrash/SentryCrashReportTests.swift index e1d6e8c56b..0d402efa4b 100644 --- a/Tests/SentryTests/Integrations/SentryCrash/SentryCrashReportTests.swift +++ b/Tests/SentryTests/Integrations/SentryCrash/SentryCrashReportTests.swift @@ -65,6 +65,21 @@ class SentryCrashReportTests: XCTestCase { } } + func testWriteTraceContext_EndsUpInSDKScope() throws { + let scope = fixture.scope + scope.span = nil + + serializeToCrashReport(scope: scope) + writeCrashReport() + + let crashReportContents = try XCTUnwrap(FileManager.default.contents(atPath: fixture.reportPath)) + + let crashReport: CrashReport = try JSONDecoder().decode(CrashReport.self, from: crashReportContents) + + XCTAssertEqual(scope.propagationContext.traceId.sentryIdString, crashReport.sentry_sdk_scope?.traceContext?["trace_id"]) + XCTAssertEqual(scope.propagationContext.spanId.sentrySpanIdString, crashReport.sentry_sdk_scope?.traceContext?["span_id"]) + } + func testShouldWriteReason_WhenWritingNSException() throws { var monitorContext = SentryCrash_MonitorContext() @@ -225,6 +240,7 @@ class SentryCrashReportTests: XCTestCase { let user: CrashReportUser? let dist: String? let context: [String: [String: String]]? + let traceContext: [String: String]? let environment: String? let tags: [String: String]? let extra: [String: String]? diff --git a/Tests/SentryTests/Integrations/SentryCrash/SentryCrashScopeObserverTests.swift b/Tests/SentryTests/Integrations/SentryCrash/SentryCrashScopeObserverTests.swift index 3424e534f0..3c1d63502c 100644 --- a/Tests/SentryTests/Integrations/SentryCrash/SentryCrashScopeObserverTests.swift +++ b/Tests/SentryTests/Integrations/SentryCrash/SentryCrashScopeObserverTests.swift @@ -119,6 +119,31 @@ class SentryCrashScopeObserverTests: XCTestCase { XCTAssertNil(getScopeJson { $0.context }) } + + func testTraceContext() { + let sut = fixture.sut + sut.setTraceContext(TestData.traceContext) + + let expected = serialize(object: TestData.traceContext) + + XCTAssertEqual(expected, getScopeJson { $0.traceContext }) + } + + func testTraceContext_setToNil() { + let sut = fixture.sut + sut.setTraceContext(TestData.traceContext) + sut.setTraceContext(nil) + + XCTAssertNil(getScopeJson { $0.traceContext }) + } + + func testTraceContext_setEmptyDict() { + let sut = fixture.sut + sut.setTraceContext(TestData.traceContext) + sut.setTraceContext([:]) + + XCTAssertNil(getScopeJson { $0.traceContext }) + } func testFingerprint() { let sut = fixture.sut diff --git a/Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayIntegrationTests.swift b/Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayIntegrationTests.swift index 10f4b740fa..ed9a6010c5 100644 --- a/Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayIntegrationTests.swift +++ b/Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayIntegrationTests.swift @@ -348,6 +348,110 @@ class SentrySessionReplayIntegrationTests: XCTestCase { XCTAssertEqual(sessionReplay.sessionReplayId, replayId) } + func testStopBecauseOfReplayRateLimit() throws { + let rateLimiter = TestRateLimits() + SentryDependencyContainer.sharedInstance().rateLimits = rateLimiter + rateLimiter.rateLimits.append(.replay) + + startSDK(sessionSampleRate: 1, errorSampleRate: 1) + let sut = try getSut() + let sessionReplay = sut.sessionReplay + + XCTAssertTrue(sessionReplay?.isRunning ?? false) + + let videoUrl = URL(fileURLWithPath: "video.mp4") + let videoInfo = SentryVideoInfo(path: videoUrl, height: 1_024, width: 480, duration: 5, frameCount: 5, frameRate: 1, start: Date(), end: Date(), fileSize: 10, screens: []) + let replayEvent = SentryReplayEvent(eventId: SentryId(), replayStartTimestamp: Date(), replayType: .session, segmentId: 0) + + (sut as SentrySessionReplayDelegate).sessionReplayNewSegment(replayEvent: replayEvent, + replayRecording: SentryReplayRecording(segmentId: 0, video: videoInfo, extraEvents: []), + videoUrl: videoUrl) + + XCTAssertFalse(sessionReplay?.isRunning ?? true) + XCTAssertNil(sut.sessionReplay) + } + + func testStopBecauseOfAllRateLimit() throws { + let rateLimiter = TestRateLimits() + SentryDependencyContainer.sharedInstance().rateLimits = rateLimiter + rateLimiter.rateLimits.append(.all) + + startSDK(sessionSampleRate: 1, errorSampleRate: 1) + let sut = try getSut() + let sessionReplay = sut.sessionReplay + + XCTAssertTrue(sessionReplay?.isRunning ?? false) + + let videoUrl = URL(fileURLWithPath: "video.mp4") + let videoInfo = SentryVideoInfo(path: videoUrl, height: 1_024, width: 480, duration: 5, frameCount: 5, frameRate: 1, start: Date(), end: Date(), fileSize: 10, screens: []) + let replayEvent = SentryReplayEvent(eventId: SentryId(), replayStartTimestamp: Date(), replayType: .session, segmentId: 0) + + (sut as SentrySessionReplayDelegate).sessionReplayNewSegment(replayEvent: replayEvent, + replayRecording: SentryReplayRecording(segmentId: 0, video: videoInfo, extraEvents: []), + videoUrl: videoUrl) + + XCTAssertFalse(sessionReplay?.isRunning ?? true) + XCTAssertNil(sut.sessionReplay) + } + + func testDontRestartAfterRateLimit() throws { + let rateLimiter = TestRateLimits() + SentryDependencyContainer.sharedInstance().rateLimits = rateLimiter + rateLimiter.rateLimits.append(.all) + + startSDK(sessionSampleRate: 1, errorSampleRate: 1) + let sut = try getSut() + let sessionReplay = sut.sessionReplay + + XCTAssertTrue(sessionReplay?.isRunning ?? false) + + let videoUrl = URL(fileURLWithPath: "video.mp4") + let videoInfo = SentryVideoInfo(path: videoUrl, height: 1_024, width: 480, duration: 5, frameCount: 5, frameRate: 1, start: Date(), end: Date(), fileSize: 10, screens: []) + let replayEvent = SentryReplayEvent(eventId: SentryId(), replayStartTimestamp: Date(), replayType: .session, segmentId: 0) + + (sut as SentrySessionReplayDelegate).sessionReplayNewSegment(replayEvent: replayEvent, + replayRecording: SentryReplayRecording(segmentId: 0, video: videoInfo, extraEvents: []), + videoUrl: videoUrl) + + XCTAssertFalse(sessionReplay?.isRunning ?? true) + XCTAssertNil(sut.sessionReplay) + + sut.start() + + XCTAssertFalse(sessionReplay?.isRunning ?? true) + XCTAssertNil(sut.sessionReplay) + } + + func testAlowStartForNewSessionAfterRateLimit() throws { + let rateLimiter = TestRateLimits() + SentryDependencyContainer.sharedInstance().rateLimits = rateLimiter + rateLimiter.rateLimits.append(.all) + + startSDK(sessionSampleRate: 0, errorSampleRate: 1) + let sut = try getSut() + let sessionReplay = sut.sessionReplay + sut.start() + + XCTAssertTrue(sessionReplay?.isRunning ?? false) + + let videoUrl = URL(fileURLWithPath: "video.mp4") + let videoInfo = SentryVideoInfo(path: videoUrl, height: 1_024, width: 480, duration: 5, frameCount: 5, frameRate: 1, start: Date(), end: Date(), fileSize: 10, screens: []) + let replayEvent = SentryReplayEvent(eventId: SentryId(), replayStartTimestamp: Date(), replayType: .session, segmentId: 0) + + (sut as SentrySessionReplayDelegate).sessionReplayNewSegment(replayEvent: replayEvent, + replayRecording: SentryReplayRecording(segmentId: 0, video: videoInfo, extraEvents: []), + videoUrl: videoUrl) + XCTAssertNil(sut.sessionReplay) + + sut.start() + XCTAssertNil(sut.sessionReplay) + + (sut as SentrySessionListener).sentrySessionStarted(SentrySession(releaseName: "", distinctId: "")) + + sut.start() + XCTAssertTrue(sut.sessionReplay?.isRunning ?? false) + } + func testStartWithBufferSessionReplay() throws { startSDK(sessionSampleRate: 0, errorSampleRate: 1) let sut = try getSut() diff --git a/Tests/SentryTests/Networking/SentryTransportAdapterTests.swift b/Tests/SentryTests/Networking/SentryTransportAdapterTests.swift index fec2e195ed..ea7990a839 100644 --- a/Tests/SentryTests/Networking/SentryTransportAdapterTests.swift +++ b/Tests/SentryTests/Networking/SentryTransportAdapterTests.swift @@ -45,7 +45,7 @@ class SentryTransportAdapterTests: XCTestCase { SentryEnvelopeItem(session: session) ]) - try assertEnvelope(expected: expectedEnvelope) + try assertSentEnvelope(expected: expectedEnvelope) } func testSendFaultyAttachment_FaultyAttachmentGetsDropped() throws { @@ -57,7 +57,7 @@ class SentryTransportAdapterTests: XCTestCase { SentryEnvelopeItem(attachment: fixture.attachment, maxAttachmentSize: fixture.options.maxAttachmentSize)! ]) - try assertEnvelope(expected: expectedEnvelope) + try assertSentEnvelope(expected: expectedEnvelope) } func testSendUserFeedback_SendsUserFeedbackEnvelope() throws { @@ -66,16 +66,38 @@ class SentryTransportAdapterTests: XCTestCase { let expectedEnvelope = SentryEnvelope(userFeedback: userFeedback) - try assertEnvelope(expected: expectedEnvelope) + try assertSentEnvelope(expected: expectedEnvelope) } - private func assertEnvelope(expected: SentryEnvelope) throws { + func testStoreEvent_StoresCorrectEnvelope() throws { + let event = TestData.event + sut.store(event, traceContext: nil) + + let expectedEnvelope = SentryEnvelope(id: event.eventId, items: [ + SentryEnvelopeItem(event: event) + ]) + + try assertStoredEnvelope(expected: expectedEnvelope) + } + + private func assertStoredEnvelope(expected: SentryEnvelope) throws { + XCTAssertEqual(self.fixture.transport1.storedEnvelopes.count, 1) + XCTAssertEqual(self.fixture.transport2.storedEnvelopes.count, 1) + + let actual = try XCTUnwrap(fixture.transport1.storedEnvelopes.first) + try assertEnvelope(expected: expected, actual: actual) + } + + private func assertSentEnvelope(expected: SentryEnvelope) throws { XCTAssertEqual(self.fixture.transport1.sentEnvelopes.count, 1) XCTAssertEqual(self.fixture.transport2.sentEnvelopes.count, 1) - let actual = fixture.transport1.sentEnvelopes.first! - XCTAssertNotNil(actual) + let actual = try XCTUnwrap(fixture.transport1.sentEnvelopes.first) + try assertEnvelope(expected: expected, actual: actual) + } + + private func assertEnvelope(expected: SentryEnvelope, actual: SentryEnvelope) throws { XCTAssertEqual(expected.header.eventId, actual.header.eventId) XCTAssertEqual(expected.header.sdkInfo, actual.header.sdkInfo) XCTAssertEqual(expected.items.count, actual.items.count) @@ -98,5 +120,6 @@ class SentryTransportAdapterTests: XCTestCase { let actualSerialized = try XCTUnwrap(SentrySerialization.data(with: actual)) XCTAssertEqual(try XCTUnwrap(SentrySerialization.data(with: expected)), actualSerialized) + } } diff --git a/Tests/SentryTests/Networking/SentryTransportFactoryTests.swift b/Tests/SentryTests/Networking/SentryTransportFactoryTests.swift index e98560a01c..96659a7ac6 100644 --- a/Tests/SentryTests/Networking/SentryTransportFactoryTests.swift +++ b/Tests/SentryTests/Networking/SentryTransportFactoryTests.swift @@ -5,7 +5,7 @@ import XCTest class SentryTransportFactoryTests: XCTestCase { private static let dsnAsString = TestConstants.dsnAsString(username: "SentryTransportFactoryTests") - + func testIntegration_UrlSessionDelegate_PassedToRequestManager() throws { let urlSessionDelegateSpy = UrlSessionDelegateSpy() @@ -19,7 +19,7 @@ class SentryTransportFactoryTests: XCTestCase { options.urlSessionDelegate = urlSessionDelegateSpy let fileManager = try! SentryFileManager(options: options, dispatchQueueWrapper: TestSentryDispatchQueueWrapper()) - let transports = TransportInitializer.initTransports(options, sentryFileManager: fileManager, currentDateProvider: TestCurrentDateProvider()) + let transports = TransportInitializer.initTransports(options, sentryFileManager: fileManager, rateLimits: rateLimiting()) let httpTransport = transports.first let requestManager = try XCTUnwrap(Dynamic(httpTransport).requestManager.asObject as? SentryQueueableRequestManager) @@ -44,7 +44,7 @@ class SentryTransportFactoryTests: XCTestCase { options.urlSession = sessionConfiguration let fileManager = try! SentryFileManager(options: options, dispatchQueueWrapper: TestSentryDispatchQueueWrapper()) - let transports = TransportInitializer.initTransports(options, sentryFileManager: fileManager, currentDateProvider: TestCurrentDateProvider()) + let transports = TransportInitializer.initTransports(options, sentryFileManager: fileManager, rateLimits: rateLimiting()) let httpTransport = transports.first let requestManager = try XCTUnwrap(Dynamic(httpTransport).requestManager.asObject as? SentryQueueableRequestManager) @@ -60,7 +60,7 @@ class SentryTransportFactoryTests: XCTestCase { func testShouldReturnTwoTransports_WhenSpotlightEnabled() throws { let options = Options() options.enableSpotlight = true - let transports = TransportInitializer.initTransports(options, sentryFileManager: try SentryFileManager(options: options), currentDateProvider: TestCurrentDateProvider()) + let transports = TransportInitializer.initTransports(options, sentryFileManager: try SentryFileManager(options: options), rateLimits: rateLimiting()) XCTAssert(transports.contains { $0.isKind(of: SentrySpotlightTransport.self) @@ -70,5 +70,13 @@ class SentryTransportFactoryTests: XCTestCase { $0.isKind(of: SentryHttpTransport.self) }) } + + func rateLimiting() -> RateLimits { + let dateProvider = TestCurrentDateProvider() + let retryAfterHeaderParser = RetryAfterHeaderParser(httpDateParser: HttpDateParser(), currentDateProvider: dateProvider) + let rateLimitParser = RateLimitParser(currentDateProvider: dateProvider) + + return DefaultRateLimits(retryAfterHeaderParser: retryAfterHeaderParser, andRateLimitParser: rateLimitParser, currentDateProvider: dateProvider) + } } diff --git a/Tests/SentryTests/Networking/SentryTransportInitializerTests.swift b/Tests/SentryTests/Networking/SentryTransportInitializerTests.swift index bd629b80be..6175b39d90 100644 --- a/Tests/SentryTests/Networking/SentryTransportInitializerTests.swift +++ b/Tests/SentryTests/Networking/SentryTransportInitializerTests.swift @@ -18,7 +18,7 @@ class SentryTransportInitializerTests: XCTestCase { func testDefault() throws { let options = try Options(dict: ["dsn": SentryTransportInitializerTests.dsnAsString]) - let result = TransportInitializer.initTransports(options, sentryFileManager: fileManager, currentDateProvider: TestCurrentDateProvider()) + let result = TransportInitializer.initTransports(options, sentryFileManager: fileManager, rateLimits: SentryDependencyContainer.sharedInstance().rateLimits) XCTAssertEqual(result.count, 1) let firstTransport = result.first diff --git a/Tests/SentryTests/Protocol/TestData.swift b/Tests/SentryTests/Protocol/TestData.swift index 4d9b5de946..f1d5e6c27b 100644 --- a/Tests/SentryTests/Protocol/TestData.swift +++ b/Tests/SentryTests/Protocol/TestData.swift @@ -14,6 +14,7 @@ class TestData { } static let sdk = ["name": SentryMeta.sdkName, "version": SentryMeta.versionString] static let context: [String: [String: Any]] = ["context": ["c": "a", "date": timestamp]] + static let traceContext: [String: [String: Any]] = ["trace": ["trace_id": "1234567890", "span_id": "1234567890"]] static let malformedURLString = "http://example.com:-80/" @@ -296,6 +297,8 @@ class TestData { crumb2.message = "Crumb 2" scope.addBreadcrumb(crumb2) + scope.span = nil + return scope } diff --git a/Tests/SentryTests/SentryClientTests.swift b/Tests/SentryTests/SentryClientTests.swift index 1eb478b3ee..2fdf98f806 100644 --- a/Tests/SentryTests/SentryClientTests.swift +++ b/Tests/SentryTests/SentryClientTests.swift @@ -1749,6 +1749,34 @@ class SentryClientTest: XCTestCase { wait(for: [callbackExpectation], timeout: 0.1) } + func testSaveCrashTransaction_StoresEventWithTraceContext() throws { + let transaction = fixture.transaction + let client = fixture.getSut() + client.saveCrashTransaction(transaction: transaction, scope: fixture.scope) + + XCTAssertEqual(fixture.transportAdapter.storeEventInvocations.first?.traceContext?.traceId, transaction.trace.traceId) + } + + func testSaveCrashTransaction_StoresEventWithScope() throws { + let transaction = fixture.transaction + let client = fixture.getSut() + client.saveCrashTransaction(transaction: transaction, scope: fixture.scope) + + let savedEvent = try XCTUnwrap(fixture.transportAdapter.storeEventInvocations.first?.event) + + XCTAssertEqual(["key": "value"], savedEvent.tags) + } + + func testSaveCrashTransaction_DisabledClient_StoresNothing() throws { + let transaction = fixture.transaction + + let client = fixture.getSutDisabledSdk() + + client.saveCrashTransaction(transaction: transaction, scope: fixture.scope) + + XCTAssertEqual(0, fixture.transportAdapter.storeEventInvocations.count) + } + func testCaptureTransactionEvent_sendTraceState() { let transaction = fixture.transaction let client = fixture.getSut() diff --git a/Tests/SentryTests/SentryHubTests.swift b/Tests/SentryTests/SentryHubTests.swift index 8a638ce39a..30a21d12f4 100644 --- a/Tests/SentryTests/SentryHubTests.swift +++ b/Tests/SentryTests/SentryHubTests.swift @@ -422,6 +422,33 @@ class SentryHubTests: XCTestCase { XCTAssertEqual(4, lostEvent?.quantity) } + func testSaveCrashTransaction_SavesTransaction() throws { + let scope = fixture.scope + let sut = SentryHub(client: fixture.client, andScope: scope) + + let transaction = sut.startTransaction(transactionContext: TransactionContext(name: fixture.transactionName, operation: fixture.transactionOperation, sampled: .yes)) + + let trans = Dynamic(transaction).toTransaction().asAnyObject + sut.saveCrash(try XCTUnwrap(trans as? Transaction)) + + let client = fixture.client + XCTAssertEqual(1, client.saveCrashTransactionInvocations.count) + XCTAssertEqual(scope, client.saveCrashTransactionInvocations.first?.scope) + XCTAssertEqual(0, client.recordLostEvents.count) + } + + func testSaveCrashTransaction_NotSampled_DoesNotSaveTransaction() throws { + let scope = fixture.scope + let sut = SentryHub(client: fixture.client, andScope: scope) + + let transaction = sut.startTransaction(transactionContext: TransactionContext(name: fixture.transactionName, operation: fixture.transactionOperation, sampled: .no)) + + let trans = Dynamic(transaction).toTransaction().asAnyObject + sut.saveCrash(try XCTUnwrap(trans as? Transaction)) + + XCTAssertEqual(self.fixture.client.saveCrashTransactionInvocations.count, 0) + } + func testCaptureMessageWithScope() { fixture.getSut().capture(message: fixture.message, scope: fixture.scope) diff --git a/Tests/SentryTests/SentryKSCrashReportConverterTests.m b/Tests/SentryTests/SentryKSCrashReportConverterTests.m index 09e9539425..a1c8ac8917 100644 --- a/Tests/SentryTests/SentryKSCrashReportConverterTests.m +++ b/Tests/SentryTests/SentryKSCrashReportConverterTests.m @@ -365,6 +365,25 @@ - (void)testUserInfo XCTAssertEqual(event.extra.count, (unsigned long)3); } +- (void)testTraceContext +{ + [self isValidReport:@"Resources/fatal-error-notable-addresses"]; + NSDictionary *rawCrash = [self getCrashReport:@"Resources/fatal-error-notable-addresses"]; + SentryCrashReportConverter *reportConverter = + [[SentryCrashReportConverter alloc] initWithReport:rawCrash inAppLogic:self.inAppLogic]; + reportConverter.userContext = @{ + @"context" : @ { @"some" : @"context" }, + @"traceContext" : @ { @"trace_id" : @"1234567890", @"span_id" : @"1234567890" } + }; + SentryEvent *event = [reportConverter convertReportToEvent]; + NSDictionary *expectedContext = @{ + @"some" : @"context", + @"trace" : @ { @"trace_id" : @"1234567890", @"span_id" : @"1234567890" } + }; + [self compareDict:expectedContext withDict:event.context]; + XCTAssertNil(event.context[@"traceContext"]); +} + /** * Uses two valid crash reports taken from a simulator, with matching scope data. */ diff --git a/Tests/SentryTests/SentryOptionsTest.m b/Tests/SentryTests/SentryOptionsTest.m index 21629bb8ee..5520ae5545 100644 --- a/Tests/SentryTests/SentryOptionsTest.m +++ b/Tests/SentryTests/SentryOptionsTest.m @@ -830,6 +830,11 @@ - (void)testEnablePerformanceV2 [self testBooleanField:@"enablePerformanceV2" defaultValue:NO]; } +- (void)testEnablePersistingTracesWhenCrashing +{ + [self testBooleanField:@"enablePersistingTracesWhenCrashing" defaultValue:NO]; +} + #if SENTRY_HAS_UIKIT - (void)testEnableUIViewControllerTracing { diff --git a/Tests/SentryTests/SentryScopeSwiftTests.swift b/Tests/SentryTests/SentryScopeSwiftTests.swift index f6ec28907b..edd073f933 100644 --- a/Tests/SentryTests/SentryScopeSwiftTests.swift +++ b/Tests/SentryTests/SentryScopeSwiftTests.swift @@ -87,7 +87,7 @@ class SentryScopeSwiftTests: XCTestCase { fixture = Fixture() } - func testSerialize() { + func testSerialize() throws { let scope = fixture.scope let actual = scope.serialize() @@ -106,8 +106,12 @@ class SentryScopeSwiftTests: XCTestCase { XCTAssertEqual(["key": "value"], actual["tags"] as? [String: String]) XCTAssertEqual(["key": "value"], actual["extra"] as? [String: String]) + XCTAssertEqual(fixture.context, actual["context"] as? [String: [String: String]]) + let propagationContext = fixture.scope.propagationContext + XCTAssertEqual(["span_id": propagationContext.spanId.sentrySpanIdString, "trace_id": propagationContext.traceId.sentryIdString], actual["traceContext"] as? [String: String]) + let actualUser = actual["user"] as? [String: Any] XCTAssertEqual(fixture.ipAddress, actualUser?["ip_address"] as? String) @@ -127,6 +131,8 @@ class SentryScopeSwiftTests: XCTestCase { let cloned = Scope(scope: scope) XCTAssertEqual(try XCTUnwrap(cloned.serialize() as? [String: AnyHashable]), snapshot) + XCTAssertEqual(scope.propagationContext.spanId, cloned.propagationContext.spanId) + XCTAssertEqual(scope.propagationContext.traceId, cloned.propagationContext.traceId) let (event1, event2) = (Event(), Event()) (event1.timestamp, event2.timestamp) = (fixture.date, fixture.date) @@ -631,6 +637,43 @@ class SentryScopeSwiftTests: XCTestCase { XCTAssertEqual(2, observer.clearBreadcrumbInvocations) } + func testScopeObserver_setSpan_SetsTraceContext() throws { + let sut = Scope() + let observer = fixture.observer + sut.add(observer) + + let transaction = fixture.transaction + sut.span = transaction + + let traceContext = try XCTUnwrap(observer.traceContext) + let serializedTransaction = transaction.serialize() + + XCTAssertEqual(Set(serializedTransaction.keys), Set(traceContext.keys)) + + XCTAssertEqual(serializedTransaction["trace_id"] as? String, traceContext["trace_id"] as? String) + XCTAssertEqual(serializedTransaction["span_id"] as? String, traceContext["span_id"] as? String) + XCTAssertEqual(serializedTransaction["op"] as? String, traceContext["op"] as? String) + XCTAssertEqual(serializedTransaction["origin"] as? String, traceContext["origin"] as? String) + XCTAssertEqual(serializedTransaction["type"] as? String, traceContext["type"] as? String) + XCTAssertEqual(serializedTransaction["start_timestamp"] as? Double, traceContext["start_timestamp"] as? Double) + XCTAssertEqual(serializedTransaction["timestamp"] as? Double, traceContext["timestamp"] as? Double) + } + + func testScopeObserver_setSpanToNil_SetsTraceContextToPropagationContext() throws { + let sut = Scope() + let observer = fixture.observer + sut.add(observer) + + sut.span = fixture.transaction + sut.span = nil + + let traceContext = try XCTUnwrap(observer.traceContext) + + XCTAssertEqual(2, traceContext.count) + XCTAssertEqual(sut.propagationContext.traceId.sentryIdString, traceContext["trace_id"] as? String) + XCTAssertEqual(sut.propagationContext.spanId.sentrySpanIdString, traceContext["span_id"] as? String) + } + func testScopeObserver_clear() { let sut = Scope() let observer = fixture.observer @@ -722,6 +765,11 @@ class SentryScopeSwiftTests: XCTestCase { self.context = context } + var traceContext: [String: Any]? + func setTraceContext(_ traceContext: [String: Any]?) { + self.traceContext = traceContext + } + var dist: String? func setDist(_ dist: String?) { self.dist = dist diff --git a/Tests/SentryTests/SentryViewPhotographerTests.swift b/Tests/SentryTests/SentryViewPhotographerTests.swift index 69643f483d..fd85fe559f 100644 --- a/Tests/SentryTests/SentryViewPhotographerTests.swift +++ b/Tests/SentryTests/SentryViewPhotographerTests.swift @@ -195,6 +195,21 @@ class SentryViewPhotographerTests: XCTestCase { assertColor(pixel1, .green) } + func testLabelInsideScrollView() throws { + let label1 = UILabel(frame: CGRect(x: 0, y: 25, width: 50, height: 25)) + label1.text = "Test" + label1.textColor = .green + + let scrollView = UIScrollView(frame: CGRect(x: 0, y: 0, width: 50, height: 50)) + scrollView.addSubview(label1) + scrollView.contentOffset = CGPoint(x: 0, y: 25) + + let image = try XCTUnwrap(prepare(views: [scrollView])) + let pixel1 = color(at: CGPoint(x: 10, y: 10), in: image) + + assertColor(pixel1, .green) + } + private func assertColor(_ color1: UIColor, _ color2: UIColor) { let sRGBColor1 = color1.cgColor.converted(to: CGColorSpace(name: CGColorSpace.sRGB)!, intent: .defaultIntent, options: nil) let sRGBColor2 = color2.cgColor.converted(to: CGColorSpace(name: CGColorSpace.sRGB)!, intent: .defaultIntent, options: nil) diff --git a/Tests/SentryTests/Transaction/SentryTracerTests.swift b/Tests/SentryTests/Transaction/SentryTracerTests.swift index c4bba33464..7ed1940a59 100644 --- a/Tests/SentryTests/Transaction/SentryTracerTests.swift +++ b/Tests/SentryTests/Transaction/SentryTracerTests.swift @@ -1309,6 +1309,88 @@ class SentryTracerTests: XCTestCase { let data = sut.data as [String: Any] XCTAssertEqual(0, data["key"] as? Int) } + + func testFinishForCrash_WithWaitForChildren_GetsFinished() { + let sut = fixture.getSut() + let child = sut.startChild(operation: "ui.load") + + advanceTime(bySeconds: 1.0) + + sut.finishForCrash() + + let currentTime = fixture.currentDateProvider.date() + + XCTAssertTrue(sut.isFinished) + XCTAssertEqual(currentTime, sut.timestamp) + + XCTAssertTrue(child.isFinished) + XCTAssertEqual(currentTime, child.timestamp) + XCTAssertEqual(SentrySpanStatus.internalError, child.status) + + XCTAssertEqual(SentrySpanStatus.internalError, sut.status) + + XCTAssertEqual(1, fixture.client.saveCrashTransactionInvocations.count) + } + + func testFinishForCrash_CallFinishTwice_OnlyOnceSaved() { + let sut = fixture.getSut() + _ = sut.startChild(operation: "ui.load") + + sut.finishForCrash() + sut.finishForCrash() + + XCTAssertEqual(1, fixture.client.saveCrashTransactionInvocations.count) + } + + func testFinishForCrash_DoesNotCancelDeadlineTimer() throws { + fixture.dispatchQueue.blockBeforeMainBlock = { true } + + let sut = fixture.getSut() + _ = sut.startChild(operation: fixture.transactionOperation) + let timer = try XCTUnwrap(Dynamic(sut).deadlineTimer.asObject as? Timer) + + sut.finishForCrash() + + XCTAssertTrue(timer.isValid) + } + + func testFinishForCrash_DoesNotCallFinishCallback() throws { + let callbackExpectation = expectation(description: "FinishCallback called") + callbackExpectation.isInverted = true + + let sut = fixture.getSut(idleTimeout: fixture.idleTimeout) + + sut.finishCallback = { tracer in + XCTAssertEqual(sut, tracer) + callbackExpectation.fulfill() + } + + sut.finishForCrash() + + wait(for: [callbackExpectation], timeout: 0.01) + } + + func testFinishForCrash_DoesNotCallTracerDidFinish() throws { + let delegate = TracerDelegate() + + let sut = fixture.getSut() + sut.delegate = delegate + + sut.finishForCrash() + + XCTAssertFalse(delegate.tracerDidFinishCalled) + } + + func testFinishForCrash_DoesNotSetSpanOnScopeToNil() throws { + + let sut = fixture.getSut() + + fixture.hub.scope.span = sut + + sut.finishForCrash() + + XCTAssertNotNil(fixture.hub.scope.span) + } private func advanceTime(bySeconds: TimeInterval) { fixture.currentDateProvider.setDate(date: fixture.currentDateProvider.date().addingTimeInterval(bySeconds))