Skip to content

Commit

Permalink
feat: Track timezone changes as breadcrumbs (#1930)
Browse files Browse the repository at this point in the history
* feat: Add automatic timezone change reporting as a breadcrumb

* Add a test for the new breadcrumb

* Format code

* Add changelog entry

* Indent with 4 spaces for all source files

* Fix testTimezoneChangeBreadcrumb by using the Swiftified name

* Actually fix the tests

* Improve the start method's logic, prevent duplicate call in case of iOS

* Undo indentation change, it'll be in a separate PR

* Feature complete, but test is flaky

* Format code

* timezoneOffset is nullable and shouldn't stop SentryAppState from being instantiated when it's missing on disk

* Only run the code in iOS, because it doesn't compile for macOS
Provide a default value to deal with test crashes, until we can properly fix the tests

* Format code

* Deal with timezoneOffset == nil

* Commit first set of suggestions

* Format code

* Add more tests

* It works, but tests need to be fixed

* Format code

* Don't cache fileManager, as it can change when the hub changes

* Use currentDateProvider

* Format code

* Undo

* Tests compile again

* Fix warning

* testTimezoneChangeBreadcrumb is still problematic

* Added more tests, and the whole suit runs successfully

* Format code

* Add more filemanager tests

* Format code

* One more test

Co-authored-by: Sentry Github Bot <bot+github-bot@sentry.io>
  • Loading branch information
kevinrenskers and getsentry-bot committed Jul 6, 2022
1 parent 47cee06 commit 53e3805
Show file tree
Hide file tree
Showing 16 changed files with 310 additions and 21 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Changelog

## Unreleased

### Features

- Track timezone changes as breadcrumbs (#1930)

## 7.19.0

### Features
Expand Down
8 changes: 7 additions & 1 deletion Sources/Sentry/SentryAutoBreadcrumbTrackingIntegration.m
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
#import "SentryAutoBreadcrumbTrackingIntegration.h"
#import "SentryBreadcrumbTracker.h"
#import "SentryDefaultCurrentDateProvider.h"
#import "SentryDependencyContainer.h"
#import "SentryFileManager.h"
#import "SentrySystemEventBreadcrumbs.h"

NS_ASSUME_NONNULL_BEGIN
Expand All @@ -22,7 +24,11 @@ - (void)installWithOptions:(nonnull SentryOptions *)options
breadcrumbTracker:[[SentryBreadcrumbTracker alloc]
initWithSwizzleWrapper:[SentryDependencyContainer sharedInstance]
.swizzleWrapper]
systemEventBreadcrumbs:[[SentrySystemEventBreadcrumbs alloc] init]];
systemEventBreadcrumbs:[[SentrySystemEventBreadcrumbs alloc]
initWithFileManager:[SentryDependencyContainer sharedInstance]
.fileManager
andCurrentDateProvider:[SentryDefaultCurrentDateProvider
sharedInstance]]];
}

/**
Expand Down
5 changes: 5 additions & 0 deletions Sources/Sentry/SentryDefaultCurrentDateProvider.m
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@ - (dispatch_time_t)dispatchTimeNow
return dispatch_time(DISPATCH_TIME_NOW, 0);
}

- (NSInteger)timezoneOffset
{
return [NSTimeZone localTimeZone].secondsFromGMT;
}

@end

NS_ASSUME_NONNULL_END
13 changes: 11 additions & 2 deletions Sources/Sentry/SentryDependencyContainer.m
Original file line number Diff line number Diff line change
Expand Up @@ -46,16 +46,25 @@ + (void)reset
}
}

- (SentryFileManager *)fileManager
{
@synchronized(sentryDependencyContainerLock) {
if (_fileManager == nil) {
_fileManager = [[[SentrySDK currentHub] getClient] fileManager];
}
return _fileManager;
}
}

- (SentryAppStateManager *)appStateManager
{
@synchronized(sentryDependencyContainerLock) {
if (_appStateManager == nil) {
SentryFileManager *fileManager = [[[SentrySDK currentHub] getClient] fileManager];
SentryOptions *options = [[[SentrySDK currentHub] getClient] options];
_appStateManager = [[SentryAppStateManager alloc]
initWithOptions:options
crashWrapper:self.crashWrapper
fileManager:fileManager
fileManager:self.fileManager
currentDateProvider:[SentryDefaultCurrentDateProvider sharedInstance]
sysctl:[[SentrySysctl alloc] init]];
}
Expand Down
63 changes: 63 additions & 0 deletions Sources/Sentry/SentryFileManager.m
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
@property (nonatomic, copy) NSString *crashedSessionFilePath;
@property (nonatomic, copy) NSString *lastInForegroundFilePath;
@property (nonatomic, copy) NSString *appStateFilePath;
@property (nonatomic, copy) NSString *timezoneOffsetFilePath;
@property (nonatomic, assign) NSUInteger currentFileCounter;
@property (nonatomic, assign) NSUInteger maxEnvelopes;
@property (nonatomic, weak) id<SentryFileManagerDelegate> delegate;
Expand Down Expand Up @@ -63,6 +64,8 @@ - (nullable instancetype)initWithOptions:(SentryOptions *)options
[self.sentryPath stringByAppendingPathComponent:@"lastInForeground.timestamp"];

self.appStateFilePath = [self.sentryPath stringByAppendingPathComponent:@"app.state"];
self.timezoneOffsetFilePath =
[self.sentryPath stringByAppendingPathComponent:@"timezone.offset"];

// Remove old cached events for versions before 6.0.0
self.eventsPath = [self.sentryPath stringByAppendingPathComponent:@"events"];
Expand Down Expand Up @@ -436,6 +439,66 @@ - (void)deleteAppState
}
}

- (NSNumber *_Nullable)readTimezoneOffset
{
[SentryLog logWithMessage:@"Reading timezone offset" andLevel:kSentryLevelDebug];
NSFileManager *fileManager = [NSFileManager defaultManager];
NSData *timezoneOffsetData = nil;
@synchronized(self.timezoneOffsetFilePath) {
timezoneOffsetData = [fileManager contentsAtPath:self.timezoneOffsetFilePath];
}
if (nil == timezoneOffsetData) {
[SentryLog logWithMessage:@"No timezone offset found." andLevel:kSentryLevelDebug];
return nil;
}
NSString *timezoneOffsetString = [[NSString alloc] initWithData:timezoneOffsetData
encoding:NSUTF8StringEncoding];

NSNumberFormatter *formatter = [[NSNumberFormatter alloc] init];
formatter.numberStyle = NSNumberFormatterDecimalStyle;

return [formatter numberFromString:timezoneOffsetString];
}

- (void)storeTimezoneOffset:(NSInteger)offset
{
NSError *error = nil;
NSString *timezoneOffsetString = [NSString stringWithFormat:@"%zd", offset];
NSString *logMessage =
[NSString stringWithFormat:@"Persisting timezone offset: %@", timezoneOffsetString];
[SentryLog logWithMessage:logMessage andLevel:kSentryLevelDebug];
@synchronized(self.timezoneOffsetFilePath) {
[[timezoneOffsetString dataUsingEncoding:NSUTF8StringEncoding]
writeToFile:self.timezoneOffsetFilePath
options:NSDataWritingAtomic
error:&error];

if (error != nil) {
[SentryLog
logWithMessage:[NSString
stringWithFormat:@"Failed to store timezone offset: %@", error]
andLevel:kSentryLevelError];
}
}
}

- (void)deleteTimezoneOffset
{
NSError *error = nil;
NSFileManager *fileManager = [NSFileManager defaultManager];
@synchronized(self.timezoneOffsetFilePath) {
[fileManager removeItemAtPath:self.timezoneOffsetFilePath error:&error];

// We don't want to log an error if the file doesn't exist.
if (nil != error && error.code != NSFileNoSuchFileError) {
[SentryLog
logWithMessage:[NSString
stringWithFormat:@"Failed to delete timezone offset %@", error]
andLevel:kSentryLevelError];
}
}
}

+ (BOOL)createDirectoryAtPath:(NSString *)path withError:(NSError **)error
{
NSFileManager *fileManager = [NSFileManager defaultManager];
Expand Down
73 changes: 73 additions & 0 deletions Sources/Sentry/SentrySystemEventBreadcrumbs.m
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
#import "SentrySystemEventBreadcrumbs.h"
#import "SentryAppState.h"
#import "SentryAppStateManager.h"
#import "SentryBreadcrumb.h"
#import "SentryCurrentDateProvider.h"
#import "SentryDependencyContainer.h"
#import "SentryLog.h"
#import "SentrySDK.h"

Expand All @@ -8,8 +12,24 @@
# import <UIKit/UIKit.h>
#endif

@interface
SentrySystemEventBreadcrumbs ()
@property (nonatomic, strong) SentryFileManager *fileManager;
@property (nonatomic, strong) id<SentryCurrentDateProvider> currentDateProvider;
@end

@implementation SentrySystemEventBreadcrumbs

- (instancetype)initWithFileManager:(SentryFileManager *)fileManager
andCurrentDateProvider:(id<SentryCurrentDateProvider>)currentDateProvider
{
if (self = [super init]) {
_fileManager = fileManager;
_currentDateProvider = currentDateProvider;
}
return self;
}

- (void)start
{
#if TARGET_OS_IOS
Expand Down Expand Up @@ -45,6 +65,7 @@ - (void)start:(UIDevice *)currentDevice
}
[self initKeyboardVisibilityObserver];
[self initScreenshotObserver];
[self initTimezoneObserver];
}
#endif

Expand Down Expand Up @@ -177,6 +198,58 @@ - (void)initScreenshotObserver
name:UIApplicationUserDidTakeScreenshotNotification
object:nil];
}

- (void)initTimezoneObserver
{
// Detect if the stored timezone is different from the current one;
// if so, then we also send a breadcrumb
NSNumber *_Nullable storedTimezoneOffset = [self.fileManager readTimezoneOffset];

if (storedTimezoneOffset == nil) {
[self updateStoredTimezone];
} else if (storedTimezoneOffset.doubleValue != self.currentDateProvider.timezoneOffset) {
[self timezoneEventTriggered:storedTimezoneOffset];
}

// Posted when the timezone of the device changed
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(timezoneEventTriggered)
name:NSSystemTimeZoneDidChangeNotification
object:nil];
}

- (void)timezoneEventTriggered
{
[self timezoneEventTriggered:nil];
}

- (void)timezoneEventTriggered:(NSNumber *)storedTimezoneOffset
{
if (storedTimezoneOffset == nil) {
storedTimezoneOffset = [self.fileManager readTimezoneOffset];
}

SentryBreadcrumb *crumb = [[SentryBreadcrumb alloc] initWithLevel:kSentryLevelInfo
category:@"device.event"];

NSInteger offset = self.currentDateProvider.timezoneOffset;

crumb.type = @"system";
crumb.data = @{
@"action" : @"TIMEZONE_CHANGE",
@"previous_seconds_from_gmt" : storedTimezoneOffset,
@"current_seconds_from_gmt" : @(offset)
};
[SentrySDK addBreadcrumb:crumb];

[self updateStoredTimezone];
}

- (void)updateStoredTimezone
{
[self.fileManager storeTimezoneOffset:self.currentDateProvider.timezoneOffset];
}

#endif

@end
2 changes: 2 additions & 0 deletions Sources/Sentry/include/SentryCurrentDateProvider.h
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ NS_SWIFT_NAME(CurrentDateProvider)

- (dispatch_time_t)dispatchTimeNow;

- (NSInteger)timezoneOffset;

@end

NS_ASSUME_NONNULL_END
2 changes: 2 additions & 0 deletions Sources/Sentry/include/SentryDependencyContainer.h
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#import "SentryDefines.h"
#import "SentryFileManager.h"
#import "SentryRandom.h"
#import <Foundation/Foundation.h>

Expand All @@ -21,6 +22,7 @@ SENTRY_NO_INIT
*/
+ (void)reset;

@property (nonatomic, strong) SentryFileManager *fileManager;
@property (nonatomic, strong) SentryAppStateManager *appStateManager;
@property (nonatomic, strong) SentryCrashWrapper *crashWrapper;
@property (nonatomic, strong) SentryThreadWrapper *threadWrapper;
Expand Down
4 changes: 4 additions & 0 deletions Sources/Sentry/include/SentryFileManager.h
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,10 @@ SENTRY_NO_INIT
- (SentryAppState *_Nullable)readAppState;
- (void)deleteAppState;

- (NSNumber *_Nullable)readTimezoneOffset;
- (void)storeTimezoneOffset:(NSInteger)offset;
- (void)deleteTimezoneOffset;

@end

@protocol SentryFileManagerDelegate <NSObject>
Expand Down
6 changes: 6 additions & 0 deletions Sources/Sentry/include/SentrySystemEventBreadcrumbs.h
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
#import "SentryAppStateManager.h"
#import "SentryCurrentDateProvider.h"
#import <Foundation/Foundation.h>

#if TARGET_OS_IOS
# import <UIKit/UIKit.h>
#endif

@interface SentrySystemEventBreadcrumbs : NSObject
SENTRY_NO_INIT

- (instancetype)initWithFileManager:(SentryFileManager *)fileManager
andCurrentDateProvider:(id<SentryCurrentDateProvider>)currentDateProvider;

- (void)start;

Expand Down
3 changes: 1 addition & 2 deletions Tests/SentryTests/Helper/SentryAppStateTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ class SentryAppStateTests: XCTestCase {
withValue { $0["was_terminated"] = nil }
withValue { $0["is_anr_ongoing"] = nil }
}

func testInitWithJSON_IfJsonContainsWrongField_AppStateIsNil() {
withValue { $0["release_name"] = 0 }
withValue { $0["os_version"] = nil }
Expand Down Expand Up @@ -72,5 +72,4 @@ class SentryAppStateTests: XCTestCase {
setValue(&serialized)
XCTAssertNil(SentryAppState(jsonObject: serialized))
}

}
2 changes: 2 additions & 0 deletions Tests/SentryTests/Helper/SentryFileManager+TestProperties.h
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ SentryFileManager (TestProperties)

@property (nonatomic, copy) NSString *envelopesPath;

@property (nonatomic, copy) NSString *timezoneOffsetFilePath;

@end

NS_ASSUME_NONNULL_END

0 comments on commit 53e3805

Please sign in to comment.