Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
### Fixes

- Propagate configured SDK info from options to events (#1853)
- Stop reporting pre warmed app starts (#1896)

## 7.16.1

Expand Down
94 changes: 55 additions & 39 deletions Sources/Sentry/SentryAppStartTracker.m
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@

static NSDate *runtimeInit = nil;

static const NSTimeInterval SENTRY_APP_START_MAX_DURATION = 5.0;

@interface
SentryAppStartTracker ()

Expand Down Expand Up @@ -86,56 +88,70 @@ - (void)start
- (void)buildAppStartMeasurement
{
void (^block)(void) = ^(void) {
[self stop];

SentryAppStartType appStartType = [self getStartType];

if (appStartType == SentryAppStartTypeUnknown) {
[SentryLog logWithMessage:@"Unknown start type. Not measuring app start."
andLevel:kSentryLevelWarning];
} else if (self.wasInBackground) {
return;
}

if (self.wasInBackground) {
// If the app was already running in the background it's not a cold or warm
// start.
[SentryLog logWithMessage:@"App was in background. Not measuring app start."
andLevel:kSentryLevelInfo];
} else {
// 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. Therefore it is
// fine to use the process start timestamp. Instead on Android the process
// can be forked before the app is launched and this would give wrong values.
// Using the proess start time returned valid values when testing with real
// devices.
// It could be that we have to switch back to setting a appStart-timestamp in
// the load method of this class to get a close approximation of when the
// process was started.
NSTimeInterval appStartDuration =
[[self.currentDate date] timeIntervalSinceDate:self.sysctl.processStartTimestamp];

// On HybridSDKs, we miss the didFinishLaunchNotification and the
// didBecomeVisibleNotification. Therefore, we can't set the
// didFinishLaunchingTimestamp, and we can't calculate the appStartDuration. Instead,
// the SDK provides the information we know and leaves the rest to the HybridSDKs.
if (PrivateSentrySDKOnly.appStartMeasurementHybridSDKMode) {
self.didFinishLaunchingTimestamp =
[[NSDate alloc] initWithTimeIntervalSinceReferenceDate:0];

appStartDuration = 0;
}

SentryAppStartMeasurement *appStartMeasurement =
[[SentryAppStartMeasurement alloc] initWithType:appStartType
appStartTimestamp:self.sysctl.processStartTimestamp
duration:appStartDuration
runtimeInitTimestamp:runtimeInit
didFinishLaunchingTimestamp:self.didFinishLaunchingTimestamp];

SentrySDK.appStartMeasurement = appStartMeasurement;
return;
}

[self stop];
// 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. Therefore we use the
// process start timestamp only if it's not too long ago. The process start time returned
// valid values when testing with real devices before iOS 15. See:
// 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];

if (appStartDuration >= SENTRY_APP_START_MAX_DURATION) {
NSString *message = [NSString
stringWithFormat:
@"The app start exceeded the max duration of %f seconds. Not measuring app "
@"start.\nThis could be because the OS prewarmed the app's process.",
SENTRY_APP_START_MAX_DURATION];
[SentryLog logWithMessage:message andLevel:kSentryLevelInfo];
return;
}

// On HybridSDKs, we miss the didFinishLaunchNotification and the
// didBecomeVisibleNotification. Therefore, we can't set the
// didFinishLaunchingTimestamp, and we can't calculate the appStartDuration. Instead,
// the SDK provides the information we know and leaves the rest to the HybridSDKs.
if (PrivateSentrySDKOnly.appStartMeasurementHybridSDKMode) {
self.didFinishLaunchingTimestamp =
[[NSDate alloc] initWithTimeIntervalSinceReferenceDate:0];

appStartDuration = 0;
}

SentryAppStartMeasurement *appStartMeasurement =
[[SentryAppStartMeasurement alloc] initWithType:appStartType
appStartTimestamp:self.sysctl.processStartTimestamp
duration:appStartDuration
runtimeInitTimestamp:runtimeInit
didFinishLaunchingTimestamp:self.didFinishLaunchingTimestamp];

SentrySDK.appStartMeasurement = appStartMeasurement;
};

// With only running this once we know that the process is a new one when the following code is
// executed.
// With only running this once we know that the process is a new one when the following
// code is executed.
// We need to make sure the block runs on each test instead of only once
# if TEST
block();
Expand Down Expand Up @@ -181,8 +197,8 @@ - (SentryAppStartType)getStartType
}

// This should never be reached as we unsubscribe to didBecomeActive after it is called the
// first time. If the previous boot time is in the future most likely the system time changed
// and we can't to anything.
// first time. If the previous boot time is in the future most likely the system time
// changed and we can't to anything.
return SentryAppStartTypeUnknown;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,13 @@ class SentryAppStartTrackerTests: XCTestCase {
assertNoAppStartUp()
}

func testAppLaunches_OSPrewarmedProcess_NoAppStartUp() {
let processStartTime = fixture.currentDate.date().addingTimeInterval(-60)
startApp(processStartTimeStamp: processStartTime)

assertNoAppStartUp()
}

func testAppLaunchesBackgroundTask_NoAppStartUp() {
sut = fixture.sut
sut.start()
Expand Down Expand Up @@ -195,8 +202,8 @@ class SentryAppStartTrackerTests: XCTestCase {
givenPreviousAppState(appState: appState)
}

private func givenProcessStartTimestamp() {
fixture.sysctl.setProcessStartTimestamp(value: fixture.currentDate.date())
private func givenProcessStartTimestamp(processStartTimeStamp: Date? = nil) {
fixture.sysctl.setProcessStartTimestamp(value: processStartTimeStamp ?? fixture.currentDate.date())
}

private func givenRuntimeInitTimestamp(sut: SentryAppStartTracker) {
Expand All @@ -209,8 +216,8 @@ class SentryAppStartTrackerTests: XCTestCase {
advanceTime(bySeconds: 0.3)
}

private func startApp() {
givenProcessStartTimestamp()
private func startApp(processStartTimeStamp: Date? = nil) {
givenProcessStartTimestamp(processStartTimeStamp: processStartTimeStamp)

sut = fixture.sut
givenRuntimeInitTimestamp(sut: sut)
Expand Down