diff --git a/CHANGELOG.md b/CHANGELOG.md index f3559eac8a0..7de6de73ac8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### Features + +- Report pre-warmed app starts (#1969) + ### Fixes - Too long flush duration (#2370) diff --git a/Samples/iOS-Swift/iOS-Swift/AppDelegate.swift b/Samples/iOS-Swift/iOS-Swift/AppDelegate.swift index 4899fe7f760..69290edc2b6 100644 --- a/Samples/iOS-Swift/iOS-Swift/AppDelegate.swift +++ b/Samples/iOS-Swift/iOS-Swift/AppDelegate.swift @@ -36,6 +36,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { // the benchmark test starts and stops a custom transaction using a UIButton, and automatic user interaction tracing stops the transaction that begins with that button press after the idle timeout elapses, stopping the profiler (only one profiler runs regardless of the number of concurrent transactions) options.enableUserInteractionTracing = !isBenchmarking options.enableAutoPerformanceTracking = !isBenchmarking + options.enablePreWarmedAppStartTracking = !isBenchmarking // because we run CPU for 15 seconds at full throttle, we trigger ANR issues being sent. disable such during benchmarks. options.enableAppHangTracking = !isBenchmarking diff --git a/Sentry.xcodeproj/project.pbxproj b/Sentry.xcodeproj/project.pbxproj index 4cec9a30249..300f34bebbd 100644 --- a/Sentry.xcodeproj/project.pbxproj +++ b/Sentry.xcodeproj/project.pbxproj @@ -296,6 +296,9 @@ 7B0A542E2521C62400A71716 /* SentryFrameRemoverTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B0A542D2521C62400A71716 /* SentryFrameRemoverTests.swift */; }; 7B0A5452252311CE00A71716 /* SentryBreadcrumbTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B0A5451252311CE00A71716 /* SentryBreadcrumbTests.swift */; }; 7B0A54562523178700A71716 /* SentryScopeSwiftTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B0A54552523178700A71716 /* SentryScopeSwiftTests.swift */; }; + 7B0DC72F288698F70039995F /* NSMutableDictionary+Sentry.h in Headers */ = {isa = PBXBuildFile; fileRef = 7B0DC72D288698F70039995F /* NSMutableDictionary+Sentry.h */; }; + 7B0DC730288698F70039995F /* NSMutableDictionary+Sentry.m in Sources */ = {isa = PBXBuildFile; fileRef = 7B0DC72E288698F70039995F /* NSMutableDictionary+Sentry.m */; }; + 7B0DC73428869BF40039995F /* NSMutableDictionarySentryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B0DC73328869BF40039995F /* NSMutableDictionarySentryTests.swift */; }; 7B127B0D27CF6F2300A71ED2 /* SentryANRTrackingIntegration.h in Headers */ = {isa = PBXBuildFile; fileRef = 7B127B0C27CF6F2300A71ED2 /* SentryANRTrackingIntegration.h */; }; 7B127B0F27CF6F4700A71ED2 /* SentryANRTrackingIntegration.m in Sources */ = {isa = PBXBuildFile; fileRef = 7B127B0E27CF6F4700A71ED2 /* SentryANRTrackingIntegration.m */; }; 7B14089624878F090035403D /* SentryCrashStackEntryMapper.h in Headers */ = {isa = PBXBuildFile; fileRef = 7B14089524878F090035403D /* SentryCrashStackEntryMapper.h */; }; @@ -1034,6 +1037,9 @@ 7B0A542D2521C62400A71716 /* SentryFrameRemoverTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryFrameRemoverTests.swift; sourceTree = ""; }; 7B0A5451252311CE00A71716 /* SentryBreadcrumbTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryBreadcrumbTests.swift; sourceTree = ""; }; 7B0A54552523178700A71716 /* SentryScopeSwiftTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryScopeSwiftTests.swift; sourceTree = ""; }; + 7B0DC72D288698F70039995F /* NSMutableDictionary+Sentry.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "NSMutableDictionary+Sentry.h"; path = "include/NSMutableDictionary+Sentry.h"; sourceTree = ""; }; + 7B0DC72E288698F70039995F /* NSMutableDictionary+Sentry.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "NSMutableDictionary+Sentry.m"; sourceTree = ""; }; + 7B0DC73328869BF40039995F /* NSMutableDictionarySentryTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSMutableDictionarySentryTests.swift; sourceTree = ""; }; 7B127B0C27CF6F2300A71ED2 /* SentryANRTrackingIntegration.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SentryANRTrackingIntegration.h; path = include/SentryANRTrackingIntegration.h; sourceTree = ""; }; 7B127B0E27CF6F4700A71ED2 /* SentryANRTrackingIntegration.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryANRTrackingIntegration.m; sourceTree = ""; }; 7B14089524878F090035403D /* SentryCrashStackEntryMapper.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SentryCrashStackEntryMapper.h; path = include/SentryCrashStackEntryMapper.h; sourceTree = ""; }; @@ -1676,6 +1682,8 @@ 63BE856F1ECEC6DE00DC44F5 /* NSDate+SentryExtras.m */, 63295AF31EF3C7DB002D4490 /* NSDictionary+SentrySanitize.h */, 63295AF41EF3C7DB002D4490 /* NSDictionary+SentrySanitize.m */, + 7B0DC72D288698F70039995F /* NSMutableDictionary+Sentry.h */, + 7B0DC72E288698F70039995F /* NSMutableDictionary+Sentry.m */, 861265F72404EC1500C4AFDE /* NSArray+SentrySanitize.h */, 861265F82404EC1500C4AFDE /* NSArray+SentrySanitize.m */, 7B6438A826A70F24000D0F65 /* UIViewController+Sentry.h */, @@ -2327,6 +2335,7 @@ isa = PBXGroup; children = ( 7B6438A626A70DDB000D0F65 /* UIViewControllerSentryTests.swift */, + 7B0DC73328869BF40039995F /* NSMutableDictionarySentryTests.swift */, 0A6EEADC28A657970076B469 /* UIViewRecursiveDescriptionTests.swift */, ); path = Categories; @@ -3118,6 +3127,7 @@ D867063F27C3BC2400048851 /* SentryCoreDataTracker.h in Headers */, 7B9657252683104C00C66E25 /* NSData+Sentry.h in Headers */, 7B6C5EDA264E8D860010D138 /* SentryFramesTrackingIntegration.h in Headers */, + 7B0DC72F288698F70039995F /* NSMutableDictionary+Sentry.h in Headers */, 63FE713920DA4C1100CDBAE8 /* SentryCrashMach.h in Headers */, 63EED6BE2237923600E02400 /* SentryOptions.h in Headers */, 7BD86EC5264A63F6005439DB /* SentrySysctl.h in Headers */, @@ -3312,6 +3322,7 @@ 7B3398652459C15200BD9C96 /* SentryEnvelopeRateLimit.m in Sources */, 0A2D8D9628997845008720F6 /* NSLocale+Sentry.m in Sources */, A2475E1F25FB648B007D9080 /* fishhook.c in Sources */, + 7B0DC730288698F70039995F /* NSMutableDictionary+Sentry.m in Sources */, 7BD4BD4527EB29F50071F4FF /* SentryClientReport.m in Sources */, 631E6D341EBC679C00712345 /* SentryQueueableRequestManager.m in Sources */, 7B8713B426415BAA006D6004 /* SentryAppStartTracker.m in Sources */, @@ -3733,6 +3744,7 @@ 7B965728268321CD00C66E25 /* SentryCrashScopeObserverTests.swift in Sources */, 7BD86ECB264A6DB5005439DB /* TestSysctl.swift in Sources */, 035E73CA27D57398005EEB11 /* SentryThreadHandleTests.mm in Sources */, + 7B0DC73428869BF40039995F /* NSMutableDictionarySentryTests.swift in Sources */, 7B6ADFCF26A02CAE0076C206 /* SentryCrashReportTests.swift in Sources */, D8B76B062808066D000A58C4 /* SentryScreenshotIntegrationTests.swift in Sources */, 7B8CA85726DD4E6200DD872C /* SentryNetworkTrackerIntegrationTests.swift in Sources */, diff --git a/Sources/Sentry/NSMutableDictionary+Sentry.m b/Sources/Sentry/NSMutableDictionary+Sentry.m new file mode 100644 index 00000000000..639161672fc --- /dev/null +++ b/Sources/Sentry/NSMutableDictionary+Sentry.m @@ -0,0 +1,21 @@ +#import "NSMutableDictionary+Sentry.h" + +@implementation +NSMutableDictionary (Sentry) + +- (void)mergeEntriesFromDictionary:(NSDictionary *)otherDictionary +{ + [otherDictionary enumerateKeysAndObjectsUsingBlock:^(id otherKey, id otherObj, BOOL *stop) { + if ([otherObj isKindOfClass:NSDictionary.class] && + [self[otherKey] isKindOfClass:NSDictionary.class]) { + NSMutableDictionary *mergedDict = ((NSDictionary *)self[otherKey]).mutableCopy; + [mergedDict mergeEntriesFromDictionary:(NSDictionary *)otherObj]; + self[otherKey] = mergedDict; + return; + } + + self[otherKey] = otherObj; + }]; +} + +@end diff --git a/Sources/Sentry/Public/SentryAppStartMeasurement.h b/Sources/Sentry/Public/SentryAppStartMeasurement.h index bab0f21b9e0..f908e25b214 100644 --- a/Sources/Sentry/Public/SentryAppStartMeasurement.h +++ b/Sources/Sentry/Public/SentryAppStartMeasurement.h @@ -27,6 +27,7 @@ SENTRY_NO_INIT * Initializes SentryAppStartMeasurement with the given parameters. */ - (instancetype)initWithType:(SentryAppStartType)type + isPreWarmed:(BOOL)isPreWarmed appStartTimestamp:(NSDate *)appStartTimestamp duration:(NSTimeInterval)duration runtimeInitTimestamp:(NSDate *)runtimeInitTimestamp @@ -38,6 +39,8 @@ SENTRY_NO_INIT */ @property (readonly, nonatomic, assign) SentryAppStartType type; +@property (readonly, nonatomic, assign) BOOL isPreWarmed; + /** * How long the app start took. From appStartTimestamp to when the SDK creates the * AppStartMeasurement, which is done when the OS posts UIWindowDidBecomeVisibleNotification. @@ -45,7 +48,8 @@ SENTRY_NO_INIT @property (readonly, nonatomic, assign) NSTimeInterval duration; /** - * The timestamp when the app started, which is the process start timestamp. + * The timestamp when the app started, which is the process start timestamp and for prewarmed app + * starts the moduleInitializationTimestamp. */ @property (readonly, nonatomic, strong) NSDate *appStartTimestamp; diff --git a/Sources/Sentry/Public/SentryOptions.h b/Sources/Sentry/Public/SentryOptions.h index efa1537d230..d8ecbf86bc6 100644 --- a/Sources/Sentry/Public/SentryOptions.h +++ b/Sources/Sentry/Public/SentryOptions.h @@ -240,6 +240,20 @@ NS_SWIFT_NAME(Options) */ @property (nonatomic, assign) NSTimeInterval idleTimeout; +/** + * This feature is EXPERIMENTAL. + * + * Report pre-warmed app starts by dropping the first app start spans if pre-warming paused during + * these steps. This approach will shorten the app start duration, but it represents the duration a + * user has to wait after clicking the app icon until the app is responsive. + * + * You can filter for different app start types in Discover with app_start_type:cold.prewarmed, + * app_start_type:warm.prewarmed, app_start_type:cold, and app_start_type:warm. + * + * Default value is NO + */ +@property (nonatomic, assign) BOOL enablePreWarmedAppStartTracking; + #endif /** diff --git a/Sources/Sentry/SentryAppStartMeasurement.m b/Sources/Sentry/SentryAppStartMeasurement.m index ea1369aaaf2..9b08bea66e3 100644 --- a/Sources/Sentry/SentryAppStartMeasurement.m +++ b/Sources/Sentry/SentryAppStartMeasurement.m @@ -11,6 +11,7 @@ - (instancetype)initWithType:(SentryAppStartType)type didFinishLaunchingTimestamp:(NSDate *)didFinishLaunchingTimestamp { return [self initWithType:type + isPreWarmed:NO appStartTimestamp:appStartTimestamp duration:duration runtimeInitTimestamp:runtimeInitTimestamp @@ -19,6 +20,7 @@ - (instancetype)initWithType:(SentryAppStartType)type } - (instancetype)initWithType:(SentryAppStartType)type + isPreWarmed:(BOOL)isPreWarmed appStartTimestamp:(NSDate *)appStartTimestamp duration:(NSTimeInterval)duration runtimeInitTimestamp:(NSDate *)runtimeInitTimestamp @@ -27,6 +29,7 @@ - (instancetype)initWithType:(SentryAppStartType)type { if (self = [super init]) { _type = type; + _isPreWarmed = isPreWarmed; _appStartTimestamp = appStartTimestamp; _duration = duration; _runtimeInitTimestamp = runtimeInitTimestamp; diff --git a/Sources/Sentry/SentryAppStartTracker.m b/Sources/Sentry/SentryAppStartTracker.m index cc9927c0f69..6aca705f566 100644 --- a/Sources/Sentry/SentryAppStartTracker.m +++ b/Sources/Sentry/SentryAppStartTracker.m @@ -34,6 +34,7 @@ @property (nonatomic, strong) SentrySysctl *sysctl; @property (nonatomic, assign) BOOL wasInBackground; @property (nonatomic, strong) NSDate *didFinishLaunchingTimestamp; +@property (nonatomic, assign) BOOL enablePreWarmedAppStartTracking; @end @@ -55,6 +56,7 @@ - (instancetype)initWithCurrentDateProvider:(id)curre dispatchQueueWrapper:(SentryDispatchQueueWrapper *)dispatchQueueWrapper appStateManager:(SentryAppStateManager *)appStateManager sysctl:(SentrySysctl *)sysctl + enablePreWarmedAppStartTracking:(BOOL)enablePreWarmedAppStartTracking { if (self = [super init]) { self.currentDate = currentDateProvider; @@ -64,6 +66,7 @@ - (instancetype)initWithCurrentDateProvider:(id)curre self.previousAppState = [self.appStateManager loadPreviousAppState]; self.wasInBackground = NO; self.didFinishLaunchingTimestamp = [currentDateProvider date]; + self.enablePreWarmedAppStartTracking = enablePreWarmedAppStartTracking; } return self; } @@ -120,12 +123,17 @@ - (void)buildAppStartMeasurement void (^block)(void) = ^(void) { [self stop]; - // Don't (yet) report pre warmed app starts. - // Check if prewarm is available. Just to be safe to not drop app start data on earlier OS - // verions. + BOOL isPreWarmed = NO; if ([self isActivePrewarmAvailable] && isActivePrewarm) { - SENTRY_LOG_INFO(@"The app was prewarmed. Not measuring app start."); - return; + SENTRY_LOG_INFO(@"The app was prewarmed."); + + if (self.enablePreWarmedAppStartTracking) { + isPreWarmed = YES; + } else { + SENTRY_LOG_INFO( + @"EnablePreWarmedAppStartTracking disabled. Not measuring app start."); + return; + } } SentryAppStartType appStartType = [self getStartType]; @@ -145,16 +153,29 @@ - (void)buildAppStartMeasurement // According to a talk at WWDC about optimizing app launch // (https://devstreaming-cdn.apple.com/videos/wwdc/2019/423lzf3qsjedrzivc7/423/423_optimizing_app_launch.pdf?dl=1 // slide 17) no process exists for cold and warm launches. Since iOS 15, though, the system - // might decide to pre-warm your app before the user tries to open it. The process start - // time returned valid values when testing with real devices if the app start is not - // prewarmed. See: + // might decide to pre-warm your app before the user tries to open it. + // Prewarming can stop at any of the app launch steps. Our findings show that most of + // the prewarmed app starts don't call the main method. Therefore we subtract the + // time before the module initialization / main method to calculate the app start + // duration. If the app start stopped during a later launch step, we drop it below with + // checking the SENTRY_APP_START_MAX_DURATION. With this approach, we will + // lose some warm app starts, but we accept this tradeoff. Useful resources: // https://developer.apple.com/documentation/uikit/app_and_environment/responding_to_the_launch_of_your_app/about_the_app_launch_sequence#3894431 // https://developer.apple.com/documentation/metrickit/mxapplaunchmetric, // https://twitter.com/steipete/status/1466013492180312068, // https://github.com/MobileNativeFoundation/discussions/discussions/146 - - NSTimeInterval appStartDuration = - [[self.currentDate date] timeIntervalSinceDate:self.sysctl.processStartTimestamp]; + // https://eisel.me/startup + NSTimeInterval appStartDuration = 0.0; + NSDate *appStartTimestamp; + if (isPreWarmed) { + appStartDuration = [[self.currentDate date] + timeIntervalSinceDate:self.sysctl.moduleInitializationTimestamp]; + appStartTimestamp = self.sysctl.moduleInitializationTimestamp; + } else { + appStartDuration = + [[self.currentDate date] timeIntervalSinceDate:self.sysctl.processStartTimestamp]; + appStartTimestamp = self.sysctl.processStartTimestamp; + } // Safety check to not report app starts that are completely off. if (appStartDuration >= SENTRY_APP_START_MAX_DURATION) { @@ -177,7 +198,8 @@ - (void)buildAppStartMeasurement SentryAppStartMeasurement *appStartMeasurement = [[SentryAppStartMeasurement alloc] initWithType:appStartType - appStartTimestamp:self.sysctl.processStartTimestamp + isPreWarmed:isPreWarmed + appStartTimestamp:appStartTimestamp duration:appStartDuration runtimeInitTimestamp:runtimeInit moduleInitializationTimestamp:self.sysctl.moduleInitializationTimestamp diff --git a/Sources/Sentry/SentryAppStartTrackingIntegration.m b/Sources/Sentry/SentryAppStartTrackingIntegration.m index c1662b6e321..dac4594938a 100644 --- a/Sources/Sentry/SentryAppStartTrackingIntegration.m +++ b/Sources/Sentry/SentryAppStartTrackingIntegration.m @@ -37,10 +37,11 @@ - (BOOL)installWithOptions:(SentryOptions *)options [SentryDependencyContainer sharedInstance].appStateManager; self.tracker = [[SentryAppStartTracker alloc] - initWithCurrentDateProvider:currentDateProvider - dispatchQueueWrapper:[[SentryDispatchQueueWrapper alloc] init] - appStateManager:appStateManager - sysctl:sysctl]; + initWithCurrentDateProvider:currentDateProvider + dispatchQueueWrapper:[[SentryDispatchQueueWrapper alloc] init] + appStateManager:appStateManager + sysctl:sysctl + enablePreWarmedAppStartTracking:options.enablePreWarmedAppStartTracking]; [self.tracker start]; return YES; diff --git a/Sources/Sentry/SentryClient.m b/Sources/Sentry/SentryClient.m index 3f916aa5ba7..3da3f62dc2e 100644 --- a/Sources/Sentry/SentryClient.m +++ b/Sources/Sentry/SentryClient.m @@ -654,6 +654,12 @@ - (void)setSdk:(SentryEvent *)event if (self.options.stitchAsyncCode) { [integrations addObject:@"StitchAsyncCode"]; } + +#if SENTRY_HAS_UIKIT + if (self.options.enablePreWarmedAppStartTracking) { + [integrations addObject:@"PreWarmedAppStartTracking"]; + } +#endif } event.sdk = @{ diff --git a/Sources/Sentry/SentryOptions.m b/Sources/Sentry/SentryOptions.m index b37bdb5369c..d8fc016fadf 100644 --- a/Sources/Sentry/SentryOptions.m +++ b/Sources/Sentry/SentryOptions.m @@ -69,6 +69,7 @@ - (instancetype)init self.attachViewHierarchy = NO; self.enableUserInteractionTracing = NO; self.idleTimeout = 3.0; + self.enablePreWarmedAppStartTracking = NO; #endif self.enableAppHangTracking = NO; self.appHangTimeoutInterval = 2.0; @@ -311,6 +312,9 @@ - (BOOL)validateOptions:(NSDictionary *)options if ([options[@"idleTimeout"] isKindOfClass:[NSNumber class]]) { self.idleTimeout = [options[@"idleTimeout"] doubleValue]; } + + [self setBool:options[@"enablePreWarmedAppStartTracking"] + block:^(BOOL value) { self->_enablePreWarmedAppStartTracking = value; }]; #endif [self setBool:options[@"enableAppHangTracking"] diff --git a/Sources/Sentry/SentryScope.m b/Sources/Sentry/SentryScope.m index 48f3b4fe41a..df43faaf00b 100644 --- a/Sources/Sentry/SentryScope.m +++ b/Sources/Sentry/SentryScope.m @@ -1,4 +1,5 @@ #import "SentryScope.h" +#import "NSMutableDictionary+Sentry.h" #import "SentryAttachment.h" #import "SentryBreadcrumb.h" #import "SentryEnvelopeItemType.h" @@ -513,13 +514,9 @@ - (SentryEvent *__nullable)applyToEvent:(SentryEvent *)event event.level = level; } - NSMutableDictionary *newContext; - if (nil == event.context) { - newContext = [self context].mutableCopy; - } else { - newContext = [NSMutableDictionary new]; - [newContext addEntriesFromDictionary:[self context]]; - [newContext addEntriesFromDictionary:event.context]; + NSMutableDictionary *newContext = [self context].mutableCopy; + if (event.context != nil) { + [newContext mergeEntriesFromDictionary:event.context]; } if (self.span != nil) { diff --git a/Sources/Sentry/SentryTracer.m b/Sources/Sentry/SentryTracer.m index b87578b68fd..d825ddd5dab 100644 --- a/Sources/Sentry/SentryTracer.m +++ b/Sources/Sentry/SentryTracer.m @@ -21,6 +21,7 @@ #import "SentryTransaction.h" #import "SentryTransactionContext.h" #import "SentryUIViewControllerPerformanceTracker.h" +#import #import #import #import @@ -626,6 +627,8 @@ - (nullable SentryAppStartMeasurement *)getAppStartMeasurement return @[]; } + NSMutableArray *appStartSpans = [NSMutableArray array]; + NSDate *appStartEndTimestamp = [appStartMeasurement.appStartTimestamp dateByAddingTimeInterval:appStartMeasurement.duration]; @@ -633,48 +636,67 @@ - (nullable SentryAppStartMeasurement *)getAppStartMeasurement operation:operation description:type]; [appStartSpan setStartTimestamp:appStartMeasurement.appStartTimestamp]; + [appStartSpan setTimestamp:appStartEndTimestamp]; - SentrySpan *premainSpan = [self buildSpan:appStartSpan.context.spanId - operation:operation - description:@"Pre Runtime Init"]; - [premainSpan setStartTimestamp:appStartMeasurement.appStartTimestamp]; - [premainSpan setTimestamp:appStartMeasurement.runtimeInitTimestamp]; + [appStartSpans addObject:appStartSpan]; - SentrySpan *runtimeInitSpan = [self buildSpan:appStartSpan.context.spanId + if (!appStartMeasurement.isPreWarmed) { + SentrySpan *premainSpan = [self buildSpan:appStartSpan.context.spanId operation:operation - description:@"Runtime Init to Pre Main Initializers"]; - [runtimeInitSpan setStartTimestamp:appStartMeasurement.runtimeInitTimestamp]; - [runtimeInitSpan setTimestamp:appStartMeasurement.moduleInitializationTimestamp]; + description:@"Pre Runtime Init"]; + [premainSpan setStartTimestamp:appStartMeasurement.appStartTimestamp]; + [premainSpan setTimestamp:appStartMeasurement.runtimeInitTimestamp]; + [appStartSpans addObject:premainSpan]; + + SentrySpan *runtimeInitSpan = [self buildSpan:appStartSpan.context.spanId + operation:operation + description:@"Runtime Init to Pre Main Initializers"]; + [runtimeInitSpan setStartTimestamp:appStartMeasurement.runtimeInitTimestamp]; + [runtimeInitSpan setTimestamp:appStartMeasurement.moduleInitializationTimestamp]; + [appStartSpans addObject:runtimeInitSpan]; + } SentrySpan *appInitSpan = [self buildSpan:appStartSpan.context.spanId operation:operation description:@"UIKit and Application Init"]; [appInitSpan setStartTimestamp:appStartMeasurement.moduleInitializationTimestamp]; [appInitSpan setTimestamp:appStartMeasurement.didFinishLaunchingTimestamp]; + [appStartSpans addObject:appInitSpan]; SentrySpan *frameRenderSpan = [self buildSpan:appStartSpan.context.spanId operation:operation description:@"Initial Frame Render"]; [frameRenderSpan setStartTimestamp:appStartMeasurement.didFinishLaunchingTimestamp]; [frameRenderSpan setTimestamp:appStartEndTimestamp]; + [appStartSpans addObject:frameRenderSpan]; - [appStartSpan setTimestamp:appStartEndTimestamp]; - - return @[ appStartSpan, premainSpan, runtimeInitSpan, appInitSpan, frameRenderSpan ]; + return appStartSpans; } - (void)addMeasurements:(SentryTransaction *)transaction { if (appStartMeasurement != nil && appStartMeasurement.type != SentryAppStartTypeUnknown) { NSString *type = nil; + NSString *appContextType = nil; if (appStartMeasurement.type == SentryAppStartTypeCold) { type = @"app_start_cold"; + appContextType = @"cold"; } else if (appStartMeasurement.type == SentryAppStartTypeWarm) { type = @"app_start_warm"; + appContextType = @"warm"; } - if (type != nil) { + if (type != nil && appContextType != nil) { [self setMeasurement:type value:@(appStartMeasurement.duration * 1000)]; + + NSString *appStartType = appStartMeasurement.isPreWarmed + ? [NSString stringWithFormat:@"%@.prewarmed", appContextType] + : appContextType; + NSMutableDictionary *context = + [[NSMutableDictionary alloc] initWithDictionary:[transaction context]]; + NSDictionary *appContext = @{ @"app" : @ { @"start_type" : appStartType } }; + [context mergeEntriesFromDictionary:appContext]; + [transaction setContext:context]; } } diff --git a/Sources/Sentry/include/NSMutableDictionary+Sentry.h b/Sources/Sentry/include/NSMutableDictionary+Sentry.h new file mode 100644 index 00000000000..b1009ff2aa0 --- /dev/null +++ b/Sources/Sentry/include/NSMutableDictionary+Sentry.h @@ -0,0 +1,17 @@ + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface +NSMutableDictionary (Sentry) + +/** + * Merges the otherDictionary into the given dictionary by overriding existing keys with the values + * of the other dictionary. + */ +- (void)mergeEntriesFromDictionary:(NSDictionary *)otherDictionary; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Sources/Sentry/include/SentryAppStartTracker.h b/Sources/Sentry/include/SentryAppStartTracker.h index 31a7ba198d4..fe88962c44c 100644 --- a/Sources/Sentry/include/SentryAppStartTracker.h +++ b/Sources/Sentry/include/SentryAppStartTracker.h @@ -19,7 +19,8 @@ SENTRY_NO_INIT - (instancetype)initWithCurrentDateProvider:(id)currentDateProvider dispatchQueueWrapper:(SentryDispatchQueueWrapper *)dispatchQueueWrapper appStateManager:(SentryAppStateManager *)appStateManager - sysctl:(SentrySysctl *)sysctl; + sysctl:(SentrySysctl *)sysctl + enablePreWarmedAppStartTracking:(BOOL)enablePreWarmedAppStartTracking; - (void)start; - (void)stop; diff --git a/Tests/SentryTests/Categories/NSMutableDictionarySentryTests.swift b/Tests/SentryTests/Categories/NSMutableDictionarySentryTests.swift new file mode 100644 index 00000000000..154baea4212 --- /dev/null +++ b/Tests/SentryTests/Categories/NSMutableDictionarySentryTests.swift @@ -0,0 +1,54 @@ +import XCTest + +class NSMutableDictionarySentryTests: XCTestCase { + + func testEmptyDictionary() { + let empty = NSMutableDictionary() + + empty.mergeEntries(from: [String: String]()) + + XCTAssertEqual(empty, NSMutableDictionary()) + } + + func testEmptyDict_AddsDict() { + let dict = NSMutableDictionary() + dict.mergeEntries(from: [0: [1: 1]]) + + XCTAssertEqual([0: [1: 1]], dict as? [Int: [Int: Int]]) + } + + func testTwoEntries_ExistingGetsMerged() { + let dict = NSMutableDictionary(dictionary: [0: [0: 0], 1: [0: 0]]) + dict.mergeEntries(from: [0: [1: 1]]) + + XCTAssertEqual([0: [0: 0, 1: 1], 1: [0: 0]], dict as? [Int: [Int: Int]]) + } + + func testTwoEntries_SameGetsOverwritten() { + let dict = NSMutableDictionary(dictionary: [0: [0: 0]]) + dict.mergeEntries(from: [0: [0: 1]]) + + XCTAssertEqual([0: [0: 1]], dict as? [Int: [Int: Int]]) + } + + func testTwoLevelsNested_ExistingGetsMerged() { + let dict = NSMutableDictionary(dictionary: [0: [0: [0: 0]]]) + dict.mergeEntries(from: [0: [0: [1: 1]]]) + + XCTAssertEqual([0: [0: [0: 0, 1: 1]]], dict as? [Int: [Int: [Int: Int]]]) + } + + func testExistingNotADict_NewIsADict_GetsOverwritten() { + let dict = NSMutableDictionary(dictionary: [0: 0]) + dict.mergeEntries(from: [0: [1: 1]]) + + XCTAssertEqual([0: [1: 1]], dict as? [Int: [Int: Int]]) + } + + func testExistingIsADict_NewIsNotADict_GetsOverwritten() { + let dict = NSMutableDictionary(dictionary: [0: [0: 0]]) + dict.mergeEntries(from: [0: 1]) + + XCTAssertEqual([0: 1], dict as? [Int: Int]) + } +} diff --git a/Tests/SentryTests/Helper/TestSysctl.swift b/Tests/SentryTests/Helper/TestSysctl.swift index 825d5afd83e..c830129cb45 100644 --- a/Tests/SentryTests/Helper/TestSysctl.swift +++ b/Tests/SentryTests/Helper/TestSysctl.swift @@ -21,4 +21,14 @@ class TestSysctl: SentrySysctl { public func setProcessStartTimestamp(value: Date) { internalProcessStartTimestamp = value } + + private var internalModuleInitializationTimestamp: Date = Date(timeIntervalSinceReferenceDate: 0) + + override var moduleInitializationTimestamp: Date { + return internalModuleInitializationTimestamp + } + + public func setModuleInitializationTimestamp(value: Date) { + internalModuleInitializationTimestamp = value + } } diff --git a/Tests/SentryTests/Integrations/Performance/AppStartTracking/SentryAppStartTrackerTests.swift b/Tests/SentryTests/Integrations/Performance/AppStartTracking/SentryAppStartTrackerTests.swift index 5616a5f7e1c..7b4eadac10f 100644 --- a/Tests/SentryTests/Integrations/Performance/AppStartTracking/SentryAppStartTrackerTests.swift +++ b/Tests/SentryTests/Integrations/Performance/AppStartTracking/SentryAppStartTrackerTests.swift @@ -15,9 +15,11 @@ class SentryAppStartTrackerTests: NotificationCenterTestCase { let crashWrapper = TestSentryCrashWrapper.sharedInstance() let appStateManager: SentryAppStateManager let dispatchQueue = TestSentryDispatchQueueWrapper() - + var enablePreWarmedAppStartTracking = true + let appStartDuration: TimeInterval = 0.4 var runtimeInitTimestamp: Date + var moduleInitializationTimestamp: Date var didFinishLaunchingTimestamp: Date init() { @@ -30,11 +32,12 @@ class SentryAppStartTrackerTests: NotificationCenterTestCase { appStateManager = SentryAppStateManager(options: options, crashWrapper: crashWrapper, fileManager: fileManager, currentDateProvider: currentDate, sysctl: sysctl, dispatchQueueWrapper: dispatchQueue) runtimeInitTimestamp = currentDate.date().addingTimeInterval(0.2) + moduleInitializationTimestamp = currentDate.date().addingTimeInterval(0.1) didFinishLaunchingTimestamp = currentDate.date().addingTimeInterval(0.3) } var sut: SentryAppStartTracker { - let sut = SentryAppStartTracker(currentDateProvider: currentDate, dispatchQueueWrapper: TestSentryDispatchQueueWrapper(), appStateManager: appStateManager, sysctl: sysctl) + let sut = SentryAppStartTracker(currentDateProvider: currentDate, dispatchQueueWrapper: TestSentryDispatchQueueWrapper(), appStateManager: appStateManager, sysctl: sysctl, enablePreWarmedAppStartTracking: enablePreWarmedAppStartTracking) return sut } } @@ -131,14 +134,33 @@ class SentryAppStartTrackerTests: NotificationCenterTestCase { assertNoAppStartUp() } - func testAppLaunches_OSPrewarmedProcess_NoAppStartUp() { + func testAppLaunches_OSPrewarmedProcess_AppStartUpShortened() { setenv("ActivePrewarm", "1", 1) SentryAppStartTracker.load() givenSystemNotRebooted() fixture.fileManager.moveAppStateToPreviousAppState() - startApp() + startApp(processStartTimeStamp: fixture.currentDate.date().addingTimeInterval(-60 * 60 * 4)) +#if os(iOS) + if #available(iOS 14.0, *) { + assertValidStart(type: .warm, expectedDuration: 0.3, preWarmed: true) + } else { + assertNoAppStartUp() + } +#else + assertNoAppStartUp() +#endif + } + + func testAppLaunches_OSPrewarmedProcess_FeatureDisabled_NoAppStartUp() { + fixture.enablePreWarmedAppStartTracking = false + setenv("ActivePrewarm", "1", 1) + SentryAppStartTracker.load() + givenSystemNotRebooted() + + fixture.fileManager.moveAppStateToPreviousAppState() + startApp() #if os(iOS) if #available(iOS 14.0, *) { assertNoAppStartUp() @@ -150,6 +172,22 @@ class SentryAppStartTrackerTests: NotificationCenterTestCase { #endif } + func testAppLaunches_OSStopsAtLaterAppLaunchStep_NoAppStartUp() { + setenv("ActivePrewarm", "1", 1) + SentryAppStartTracker.load() + givenSystemNotRebooted() + givenModuleInitializationTimestamp(timestamp: fixture.currentDate.date().addingTimeInterval(-200)) + + let currentDate = fixture.currentDate.date() + startApp( + processStartTimeStamp: currentDate.addingTimeInterval(-200.5), + runtimeInitTimestamp: currentDate.addingTimeInterval(-200.4), + moduleInitializationTimestamp: currentDate.addingTimeInterval(-200) + ) + + assertNoAppStartUp() + } + func testAppLaunches_WrongEnvValue_AppStartUp() { setenv("ActivePrewarm", "0", 1) SentryAppStartTracker.load() @@ -244,25 +282,30 @@ class SentryAppStartTrackerTests: NotificationCenterTestCase { givenPreviousAppState(appState: appState) } - private func givenProcessStartTimestamp(processStartTimeStamp: Date? = nil) { - fixture.sysctl.setProcessStartTimestamp(value: processStartTimeStamp ?? fixture.currentDate.date()) + private func givenProcessStartTimestamp(processStartTimestamp: Date? = nil) { + fixture.sysctl.setProcessStartTimestamp(value: processStartTimestamp ?? fixture.currentDate.date()) } - private func givenRuntimeInitTimestamp(sut: SentryAppStartTracker) { - fixture.runtimeInitTimestamp = fixture.currentDate.date().addingTimeInterval(0.2) + private func givenRuntimeInitTimestamp(sut: SentryAppStartTracker, timestamp: Date? = nil) { + fixture.runtimeInitTimestamp = timestamp ?? fixture.currentDate.date().addingTimeInterval(0.2) Dynamic(sut).setRuntimeInit(fixture.runtimeInitTimestamp) } + private func givenModuleInitializationTimestamp(timestamp: Date? = nil) { + fixture.sysctl.setModuleInitializationTimestamp(value: timestamp ?? fixture.moduleInitializationTimestamp) + } + private func givenDidFinishLaunchingTimestamp() { fixture.didFinishLaunchingTimestamp = fixture.currentDate.date().addingTimeInterval(0.3) advanceTime(bySeconds: 0.3) } - private func startApp(processStartTimeStamp: Date? = nil) { - givenProcessStartTimestamp(processStartTimeStamp: processStartTimeStamp) + private func startApp(processStartTimeStamp: Date? = nil, runtimeInitTimestamp: Date? = nil, moduleInitializationTimestamp: Date? = nil) { + givenProcessStartTimestamp(processStartTimestamp: processStartTimeStamp) sut = fixture.sut - givenRuntimeInitTimestamp(sut: sut) + givenRuntimeInitTimestamp(sut: sut, timestamp: runtimeInitTimestamp) + givenModuleInitializationTimestamp(timestamp: moduleInitializationTimestamp) sut.start() willEnterForeground() @@ -290,7 +333,8 @@ class SentryAppStartTrackerTests: NotificationCenterTestCase { sut = fixture.sut Dynamic(sut).setRuntimeInit(fixture.runtimeInitTimestamp) - + givenModuleInitializationTimestamp() + didFinishLaunching() advanceTime(bySeconds: 0.1) @@ -314,7 +358,7 @@ class SentryAppStartTrackerTests: NotificationCenterTestCase { SentrySDK.setAppStartMeasurement(nil) } - private func assertValidStart(type: SentryAppStartType, expectedDuration: TimeInterval? = nil) { + private func assertValidStart(type: SentryAppStartType, expectedDuration: TimeInterval? = nil, preWarmed: Bool = false) { guard let appStartMeasurement = SentrySDK.getAppStartMeasurement() else { XCTFail("AppStartMeasurement must not be nil") return @@ -326,10 +370,16 @@ class SentryAppStartTrackerTests: NotificationCenterTestCase { let actualAppStartDuration = appStartMeasurement.duration XCTAssertEqual(expectedAppStartDuration, actualAppStartDuration, accuracy: 0.000_1) - XCTAssertEqual(fixture.sysctl.processStartTimestamp, appStartMeasurement.appStartTimestamp) + if preWarmed { + XCTAssertEqual(fixture.moduleInitializationTimestamp, appStartMeasurement.appStartTimestamp) + } else { + XCTAssertEqual(fixture.sysctl.processStartTimestamp, appStartMeasurement.appStartTimestamp) + } + XCTAssertEqual(fixture.sysctl.moduleInitializationTimestamp, appStartMeasurement.moduleInitializationTimestamp) XCTAssertEqual(fixture.runtimeInitTimestamp, appStartMeasurement.runtimeInitTimestamp) XCTAssertEqual(fixture.didFinishLaunchingTimestamp, appStartMeasurement.didFinishLaunchingTimestamp) + XCTAssertEqual(preWarmed, appStartMeasurement.isPreWarmed) } private func assertValidHybridStart(type: SentryAppStartType) { diff --git a/Tests/SentryTests/Performance/SentryTracerTests.swift b/Tests/SentryTests/Performance/SentryTracerTests.swift index 6a3868afaed..53be5bcfe32 100644 --- a/Tests/SentryTests/Performance/SentryTracerTests.swift +++ b/Tests/SentryTests/Performance/SentryTracerTests.swift @@ -3,14 +3,14 @@ import XCTest class SentryTracerTests: XCTestCase { private class TracerDelegate: SentryTracerDelegate { - + var activeSpan: Span? - + func activeSpan(for tracer: SentryTracer) -> Span? { return activeSpan } } - + private class Fixture { let client: TestClient let hub: TestHub @@ -25,9 +25,9 @@ class SentryTracerTests: XCTestCase { let appStartColdOperation = "app.start.cold" let currentDateProvider = TestCurrentDateProvider() - let appStart: Date - let appStartEnd: Date - let appStartDuration = 0.5 + var appStart: Date + var appStartEnd: Date + var appStartDuration = 0.5 let testKey = "extra_key" let testValue = "extra_value" @@ -42,7 +42,7 @@ class SentryTracerTests: XCTestCase { CurrentDate.setCurrentDateProvider(currentDateProvider) appStart = currentDateProvider.date() - appStartEnd = appStart.addingTimeInterval(0.5) + appStartEnd = appStart.addingTimeInterval(appStartDuration) transactionContext = TransactionContext(name: transactionName, operation: transactionOperation) @@ -62,13 +62,18 @@ class SentryTracerTests: XCTestCase { #endif } - func getAppStartMeasurement(type: SentryAppStartType) -> SentryAppStartMeasurement { - let appStartDuration = 0.5 - let main = appStart.addingTimeInterval(0.15) - let runtimeInit = appStart.addingTimeInterval(0.05) + func getAppStartMeasurement(type: SentryAppStartType, preWarmed: Bool = false) -> SentryAppStartMeasurement { + let runtimeInitDuration = 0.05 + let runtimeInit = appStart.addingTimeInterval(runtimeInitDuration) + let mainDuration = 0.15 + let main = appStart.addingTimeInterval(mainDuration) let didFinishLaunching = appStart.addingTimeInterval(0.3) - - return SentryAppStartMeasurement(type: type, appStartTimestamp: appStart, duration: appStartDuration, runtimeInitTimestamp: runtimeInit, moduleInitializationTimestamp: main, didFinishLaunchingTimestamp: didFinishLaunching) + appStart = preWarmed ? main : appStart + appStartDuration = preWarmed ? appStartDuration - runtimeInitDuration - mainDuration : appStartDuration + + appStartEnd = appStart.addingTimeInterval(appStartDuration) + + return SentryAppStartMeasurement(type: type, isPreWarmed: preWarmed, appStartTimestamp: appStart, duration: appStartDuration, runtimeInitTimestamp: runtimeInit, moduleInitializationTimestamp: main, didFinishLaunchingTimestamp: didFinishLaunching) } func getSut(waitForChildren: Bool = true) -> SentryTracer { @@ -368,102 +373,107 @@ class SentryTracerTests: XCTestCase { let appStartMeasurement = fixture.getAppStartMeasurement(type: .cold) SentrySDK.setAppStartMeasurement(appStartMeasurement) - let sut = fixture.getSut() - sut.startTimestamp = fixture.appStartEnd.addingTimeInterval(5) - sut.finish() - fixture.hub.group.wait() - - XCTAssertEqual(1, fixture.hub.capturedEventsWithScopes.count) - let serializedTransaction = fixture.hub.capturedEventsWithScopes.first!.event.serialize() - let measurements = serializedTransaction["measurements"] as? [String: [String: Int]] - - XCTAssertEqual(["app_start_cold": ["value": 500]], measurements) - + whenFinishingAutoUITransaction(startTimestamp: 5) + + assertMeasurements(["app_start_cold": ["value": fixture.appStartDuration * 1_000]]) + let transaction = fixture.hub.capturedEventsWithScopes.first!.event as! Transaction assertAppStartsSpanAdded(transaction: transaction, startType: "Cold Start", operation: fixture.appStartColdOperation, appStartMeasurement: appStartMeasurement) } - + func test_startChildWithDelegate() { let delegate = TracerDelegate() - + let sut = fixture.getSut() sut.delegate = delegate - + let child = sut.startChild(operation: fixture.transactionOperation) - + delegate.activeSpan = child - + let secondChild = sut.startChild(operation: fixture.transactionOperation) - + XCTAssertEqual(secondChild.context.parentSpanId, child.context.spanId) } - + func test_startChildWithDelegate_ActiveNotChild() { let delegate = TracerDelegate() - + let sut = fixture.getSut() sut.delegate = delegate - + delegate.activeSpan = SentryTracer(transactionContext: TransactionContext(name: fixture.transactionName, operation: fixture.transactionOperation), hub: nil) - + let child = sut.startChild(operation: fixture.transactionOperation) - + let secondChild = sut.startChild(operation: fixture.transactionOperation) - + XCTAssertEqual(secondChild.context.parentSpanId, sut.context.spanId) XCTAssertEqual(secondChild.context.parentSpanId, child.context.parentSpanId) } - + func test_startChildWithDelegate_SelfIsActive() { let delegate = TracerDelegate() - + let sut = fixture.getSut() sut.delegate = delegate - + delegate.activeSpan = sut - + let child = sut.startChild(operation: fixture.transactionOperation) - + let secondChild = sut.startChild(operation: fixture.transactionOperation) - + XCTAssertEqual(secondChild.context.parentSpanId, sut.context.spanId) XCTAssertEqual(secondChild.context.parentSpanId, child.context.parentSpanId) } - + + func testAddPreWarmedAppStartMeasurement_PutOnNextAutoUITransaction() { + let appStartMeasurement = fixture.getAppStartMeasurement(type: .cold, preWarmed: true) + SentrySDK.setAppStartMeasurement(appStartMeasurement) + + whenFinishingAutoUITransaction(startTimestamp: 5) + + assertMeasurements(["app_start_cold": ["value": fixture.appStartDuration * 1_000]]) + + let transaction = fixture.hub.capturedEventsWithScopes.first!.event as! Transaction + assertPreWarmedAppStartsSpanAdded(transaction: transaction, startType: "Cold Start", operation: fixture.appStartColdOperation, appStartMeasurement: appStartMeasurement) + } + func testAddWarmAppStartMeasurement_PutOnNextAutoUITransaction() { let appStartMeasurement = fixture.getAppStartMeasurement(type: .warm) SentrySDK.setAppStartMeasurement(appStartMeasurement) advanceTime(bySeconds: -(fixture.appStartDuration + 4)) - + let sut = fixture.getSut() advanceTime(bySeconds: 1) sut.finish() fixture.hub.group.wait() XCTAssertEqual(1, fixture.hub.capturedEventsWithScopes.count) - + assertAppStartMeasurementOn(transaction: fixture.hub.capturedEventsWithScopes.first!.event as! Transaction, appStartMeasurement: appStartMeasurement) } - + func testAddColdStartMeasurement_PutOnFirstStartedTransaction() { let appStartMeasurement = fixture.getAppStartMeasurement(type: .warm) SentrySDK.setAppStartMeasurement(appStartMeasurement) - + advanceTime(bySeconds: 0.5) - + let firstTransaction = fixture.getSut() advanceTime(bySeconds: 0.5) - + let secondTransaction = fixture.getSut() advanceTime(bySeconds: 0.5) secondTransaction.finish() - + fixture.hub.group.wait() XCTAssertEqual(1, fixture.hub.capturedEventsWithScopes.count) let serializedSecondTransaction = fixture.hub.capturedEventsWithScopes.first!.event.serialize() XCTAssertNil(serializedSecondTransaction["measurements"]) - + firstTransaction.finish() fixture.hub.group.wait() @@ -474,6 +484,7 @@ class SentryTracerTests: XCTestCase { func testAddUnknownAppStartMeasurement_NotPutOnNextTransaction() { SentrySDK.setAppStartMeasurement(SentryAppStartMeasurement( type: SentryAppStartType.unknown, + isPreWarmed: false, appStartTimestamp: fixture.currentDateProvider.date(), duration: 0.5, runtimeInitTimestamp: fixture.currentDateProvider.date(), @@ -487,6 +498,42 @@ class SentryTracerTests: XCTestCase { assertAppStartMeasurementNotPutOnTransaction() } + func testPreWarmedColdAppStart_AddsStartTypeToContext() { + let appStartMeasurement = fixture.getAppStartMeasurement(type: .cold, preWarmed: true) + SentrySDK.setAppStartMeasurement(appStartMeasurement) + + whenFinishingAutoUITransaction(startTimestamp: 5) + + assertAppStartTypeAddedtoContext(expected: "cold.prewarmed") + } + + func testColdAppStart_AddsStartTypeToContext() { + let appStartMeasurement = fixture.getAppStartMeasurement(type: .cold, preWarmed: false) + SentrySDK.setAppStartMeasurement(appStartMeasurement) + + whenFinishingAutoUITransaction(startTimestamp: 5) + + assertAppStartTypeAddedtoContext(expected: "cold") + } + + func testPreWarmedWarmAppStart_AddsStartTypeToContext() { + let appStartMeasurement = fixture.getAppStartMeasurement(type: .warm, preWarmed: true) + SentrySDK.setAppStartMeasurement(appStartMeasurement) + + whenFinishingAutoUITransaction(startTimestamp: 5) + + assertAppStartTypeAddedtoContext(expected: "warm.prewarmed") + } + + func testPreWarmedWarmAppStart_DoesntAddStartTypeToContext() { + let appStartMeasurement = fixture.getAppStartMeasurement(type: .unknown, preWarmed: true) + SentrySDK.setAppStartMeasurement(appStartMeasurement) + + whenFinishingAutoUITransaction(startTimestamp: 5) + + assertAppStartTypeAddedtoContext(expected: nil) + } + func testAddWarmAppStartMeasurement_NotPutOnNonAutoUITransaction() { let appStartMeasurement = fixture.getAppStartMeasurement(type: .warm) SentrySDK.setAppStartMeasurement(appStartMeasurement) @@ -508,12 +555,12 @@ class SentryTracerTests: XCTestCase { XCTAssertEqual(0, spans.count) } - func testAddWarmAppStartMeasurement_TooOldTransaction_NotPutOnNonAutoUITransaction() { + func testAddWarmAppStartMeasurement_TooOldTransaction_NotPutOnTransaction() { let appStartMeasurement = fixture.getAppStartMeasurement(type: .warm) SentrySDK.setAppStartMeasurement(appStartMeasurement) advanceTime(bySeconds: fixture.appStartDuration + 5.01) - + let sut = fixture.getSut() advanceTime(bySeconds: 1.0) sut.finish() @@ -522,12 +569,12 @@ class SentryTracerTests: XCTestCase { assertAppStartMeasurementNotPutOnTransaction() } - func testAddWarmAppStartMeasurement_TooYoungTransaction_NotPutOnNonAutoUITransaction() { + func testAddWarmAppStartMeasurement_TooYoungTransaction_NotPutOnTransaction() { let appStartMeasurement = fixture.getAppStartMeasurement(type: .warm) SentrySDK.setAppStartMeasurement(appStartMeasurement) advanceTime(bySeconds: -(fixture.appStartDuration + 4.01)) - + let sut = fixture.getSut() advanceTime(bySeconds: 1.0) sut.finish() @@ -552,7 +599,7 @@ class SentryTracerTests: XCTestCase { let name = "something" let value: NSNumber = -12.34 let unit = MeasurementUnitFraction.percent - + let sut = fixture.getSut() let childSpan = sut.startChild(operation: "operation") sut.setMeasurement(name: name, value: 12.0, unit: unit) @@ -560,19 +607,19 @@ class SentryTracerTests: XCTestCase { childSpan.finish() sut.finish() fixture.hub.group.wait() - + XCTAssertEqual(1, fixture.hub.capturedEventsWithScopes.count) let serializedTransaction = fixture.hub.capturedEventsWithScopes.first?.event.serialize() - + let measurements = serializedTransaction?["measurements"] as? [String: [String: Any]] XCTAssertEqual(1, measurements?.count) - + let measurement = measurements?[name] XCTAssertNotNil(measurement) XCTAssertEqual(value, measurement?["value"] as! NSNumber) XCTAssertEqual(unit.unit, measurement?["unit"] as! String) } - + func testFinish_WithUnfinishedChildren() { CurrentDate.setCurrentDateProvider(DefaultCurrentDateProvider.sharedInstance()) let sut = fixture.getSut(waitForChildren: false) @@ -781,7 +828,7 @@ class SentryTracerTests: XCTestCase { // then XCTAssertEqual(dict, [fixture.testKey: fixture.testValue]) } - + private func advanceTime(bySeconds: TimeInterval) { fixture.currentDateProvider.setDate(date: fixture.currentDateProvider.date().addingTimeInterval(bySeconds)) @@ -797,6 +844,13 @@ class SentryTracerTests: XCTestCase { return transaction.serialize() } + private func whenFinishingAutoUITransaction(startTimestamp: TimeInterval) { + let sut = fixture.getSut() + sut.startTimestamp = fixture.appStartEnd.addingTimeInterval(startTimestamp) + sut.finish() + fixture.hub.group.wait() + } + private func assertTransactionNotCaptured(_ tracer: SentryTracer) { fixture.hub.group.wait() XCTAssertEqual(0, fixture.hub.capturedEventsWithScopes.count) @@ -837,16 +891,45 @@ class SentryTracerTests: XCTestCase { assertSpan("UIKit and Application Init", appStartMeasurement.moduleInitializationTimestamp, appStartMeasurement.didFinishLaunchingTimestamp) assertSpan("Initial Frame Render", appStartMeasurement.didFinishLaunchingTimestamp, fixture.appStartEnd) } - + private func assertAppStartMeasurementOn(transaction: Transaction, appStartMeasurement: SentryAppStartMeasurement) { let serializedTransaction = transaction.serialize() let measurements = serializedTransaction["measurements"] as? [String: [String: Int]] - - XCTAssertEqual(["app_start_warm": ["value": 500]], measurements) - + + let appStartDurationInMillis = Int(fixture.appStartDuration * 1_000) + XCTAssertEqual(["app_start_warm": ["value": appStartDurationInMillis]], measurements) + assertAppStartsSpanAdded(transaction: transaction, startType: "Warm Start", operation: fixture.appStartWarmOperation, appStartMeasurement: appStartMeasurement) } - + + private func assertPreWarmedAppStartsSpanAdded(transaction: Transaction, startType: String, operation: String, appStartMeasurement: SentryAppStartMeasurement) { + let spans: [SentrySpan]? = Dynamic(transaction).spans + XCTAssertEqual(3, spans?.count) + + let appLaunchSpan = spans?.first { span in + span.context.spanDescription == startType + } + let trace: SentryTracer? = Dynamic(transaction).trace + XCTAssertEqual(operation, appLaunchSpan?.context.operation) + XCTAssertEqual(trace?.context.spanId, appLaunchSpan?.context.parentSpanId) + XCTAssertEqual(appStartMeasurement.appStartTimestamp, appLaunchSpan?.startTimestamp) + XCTAssertEqual(fixture.appStartEnd.timeIntervalSince1970, appLaunchSpan?.timestamp?.timeIntervalSince1970) + + func assertSpan(_ description: String, _ startTimestamp: Date, _ timestamp: Date) { + let span = spans?.first { span in + span.context.spanDescription == description + } + + XCTAssertEqual(operation, span?.context.operation) + XCTAssertEqual(appLaunchSpan?.context.spanId, span?.context.parentSpanId) + XCTAssertEqual(startTimestamp, span?.startTimestamp) + XCTAssertEqual(timestamp, span?.timestamp) + } + + assertSpan("UIKit and Application Init", appStartMeasurement.moduleInitializationTimestamp, appStartMeasurement.didFinishLaunchingTimestamp) + assertSpan("Initial Frame Render", appStartMeasurement.didFinishLaunchingTimestamp, fixture.appStartEnd) + } + private func assertAppStartMeasurementNotPutOnTransaction() { XCTAssertEqual(1, fixture.hub.capturedEventsWithScopes.count) let serializedTransaction = fixture.hub.capturedEventsWithScopes.first!.event.serialize() @@ -864,4 +947,21 @@ class SentryTracerTests: XCTestCase { XCTAssertNil(serializedTransaction?["measurements"]) } + private func assertMeasurements(_ expectedMeasurements: [String: [String: Double]]) { + XCTAssertEqual(1, fixture.hub.capturedEventsWithScopes.count) + let serializedTransaction = fixture.hub.capturedEventsWithScopes.first!.event.serialize() + let measurements = serializedTransaction["measurements"] as? [String: [String: Double]] + + XCTAssertEqual(expectedMeasurements, measurements) + } + + private func assertAppStartTypeAddedtoContext(expected: String?) { + XCTAssertEqual(1, fixture.hub.capturedEventsWithScopes.count) + let serializedTransaction = fixture.hub.capturedEventsWithScopes.first!.event.serialize() + let context = serializedTransaction["contexts"] as? [String: [String: Any]] + + let appContext = context?["app"] as? [String: String] + XCTAssertEqual(expected, appContext?["start_type"]) + } + } diff --git a/Tests/SentryTests/Protocol/TestData.swift b/Tests/SentryTests/Protocol/TestData.swift index 64177f526d2..4c724327968 100644 --- a/Tests/SentryTests/Protocol/TestData.swift +++ b/Tests/SentryTests/Protocol/TestData.swift @@ -240,6 +240,6 @@ class TestData { let runtimeInit = appStartTimestamp.addingTimeInterval(0.05) let didFinishLaunching = appStartTimestamp.addingTimeInterval(0.3) - return SentryAppStartMeasurement(type: type, appStartTimestamp: appStartTimestamp, duration: appStartDuration, runtimeInitTimestamp: runtimeInit, moduleInitializationTimestamp: main, didFinishLaunchingTimestamp: didFinishLaunching) + return SentryAppStartMeasurement(type: type, isPreWarmed: false, appStartTimestamp: appStartTimestamp, duration: appStartDuration, runtimeInitTimestamp: runtimeInit, moduleInitializationTimestamp: main, didFinishLaunchingTimestamp: didFinishLaunching) } } diff --git a/Tests/SentryTests/SentryClientTests.swift b/Tests/SentryTests/SentryClientTests.swift index c3e0f5359f2..a9acfa07fed 100644 --- a/Tests/SentryTests/SentryClientTests.swift +++ b/Tests/SentryTests/SentryClientTests.swift @@ -1032,17 +1032,31 @@ class SentryClientTest: XCTestCase { } } - func testTrackStitchAsyncCode() { + func testTrackStitchAsyncCode() { + testFeatureTrackingAsIntegration(integrationName: "StitchAsyncCode") { + $0.stitchAsyncCode = true + } + } + +#if os(iOS) || os(tvOS) || targetEnvironment(macCatalyst) + func testTrackPreWarmedAppStartTracking() { + testFeatureTrackingAsIntegration(integrationName: "PreWarmedAppStartTracking") { + $0.enablePreWarmedAppStartTracking = true + } + } +#endif + + private func testFeatureTrackingAsIntegration(integrationName: String, configureOptions: (Options) -> Void) { SentrySDK.start(options: Options()) let eventId = fixture.getSut(configureOptions: { options in - options.stitchAsyncCode = true + configureOptions(options) }).capture(message: fixture.messageAsString) eventId.assertIsNotEmpty() assertLastSentEvent { actual in assertArrayEquals( - expected: ["AutoBreadcrumbTracking", "AutoSessionTracking", "Crash", "NetworkTracking", "StitchAsyncCode"], + expected: ["AutoBreadcrumbTracking", "AutoSessionTracking", "Crash", "NetworkTracking", integrationName], actual: actual.sdk?["integrations"] as? [String] ) } diff --git a/Tests/SentryTests/SentryOptionsTest.m b/Tests/SentryTests/SentryOptionsTest.m index 109dab8a7e7..4ba6a70abcc 100644 --- a/Tests/SentryTests/SentryOptionsTest.m +++ b/Tests/SentryTests/SentryOptionsTest.m @@ -760,6 +760,11 @@ - (void)testIdleTimeout XCTAssertEqual([idleTimeout doubleValue], options.idleTimeout); } +- (void)testEnablePreWarmedAppStartTracking +{ + [self testBooleanField:@"enablePreWarmedAppStartTracking" defaultValue:NO]; +} + #endif - (void)testEnableAppHangTracking diff --git a/Tests/SentryTests/SentryScopeSwiftTests.swift b/Tests/SentryTests/SentryScopeSwiftTests.swift index ee36b86426c..9313805d8bf 100644 --- a/Tests/SentryTests/SentryScopeSwiftTests.swift +++ b/Tests/SentryTests/SentryScopeSwiftTests.swift @@ -238,6 +238,35 @@ class SentryScopeSwiftTests: XCTestCase { XCTAssertEqual(context as? [String: [String: String]], actual?.context as? [String: [String: String]]) } + + func testApplyToEvent_EventWithContext_MergesContext() { + let context = NSMutableDictionary(dictionary: [ + "first": ["a": "b", "c": "d"]]) + let event = fixture.event + event.context = context as? [String: [String: String]] + + let expectedAppContext = [ + "first": [ "a": "b", "c": "d", "e": "f"], + "second": ["0": "1" ] + ] + + // The existing values from the scope get overwritten by the values of the event + // "a": [12:1] will be overwritten with "a": "b" + // "c": "c" will be overwritten with "c": "d" + // "e": "f" gets added from the scope to the event + let scope = fixture.scope + scope.setContext(value: ["a": [12: 1], "c": "c", "e": "f"], key: "first") + scope.setContext(value: ["0": "1"], key: "second") + + let actual = scope.apply(to: event, maxBreadcrumb: 10) + let actualContext = actual?.context as? [String: [String: String]] + + context.addEntries(from: fixture.context) + context.addEntries(from: expectedAppContext) + + XCTAssertEqual(context as? [String: [String: String]], + actualContext) + } func testClear() { let scope = fixture.scope diff --git a/Tests/SentryTests/SentryTests-Bridging-Header.h b/Tests/SentryTests/SentryTests-Bridging-Header.h index 8845fa563b7..919d5b32586 100644 --- a/Tests/SentryTests/SentryTests-Bridging-Header.h +++ b/Tests/SentryTests/SentryTests-Bridging-Header.h @@ -5,6 +5,7 @@ #import "NSData+Sentry.h" #import "NSData+SentryCompression.h" #import "NSDate+SentryExtras.h" +#import "NSMutableDictionary+Sentry.h" #import "NSURLProtocolSwizzle.h" #import "PrivateSentrySDKOnly.h" #import "SentryANRTracker.h"