AutorÃa | Ultima modificación | Ver Log |
// Copyright 2019 Google
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#import "Crashlytics/Crashlytics/Models/FIRCLSSettings.h"
#if __has_include(<FBLPromises/FBLPromises.h>)
#import <FBLPromises/FBLPromises.h>
#else
#import "FBLPromises.h"
#endif
#import "Crashlytics/Crashlytics/Helpers/FIRCLSLogger.h"
#import "Crashlytics/Crashlytics/Models/FIRCLSFileManager.h"
#import "Crashlytics/Crashlytics/Settings/Models/FIRCLSApplicationIdentifierModel.h"
#import "Crashlytics/Shared/FIRCLSConstants.h"
#import "Crashlytics/Shared/FIRCLSNetworking/FIRCLSURLBuilder.h"
NSString *const CreatedAtKey = @"created_at";
NSString *const GoogleAppIDKey = @"google_app_id";
NSString *const BuildInstanceID = @"build_instance_id";
NSString *const AppVersion = @"app_version";
@interface FIRCLSSettings ()
@property(nonatomic, strong) FIRCLSFileManager *fileManager;
@property(nonatomic, strong) FIRCLSApplicationIdentifierModel *appIDModel;
@property(nonatomic, strong) NSDictionary<NSString *, id> *settingsDictionary;
@property(nonatomic) BOOL isCacheKeyExpired;
@end
@implementation FIRCLSSettings
- (instancetype)initWithFileManager:(FIRCLSFileManager *)fileManager
appIDModel:(FIRCLSApplicationIdentifierModel *)appIDModel {
self = [super init];
if (!self) {
return nil;
}
_fileManager = fileManager;
_appIDModel = appIDModel;
_settingsDictionary = nil;
_isCacheKeyExpired = NO;
return self;
}
#pragma mark - Public Methods
- (void)reloadFromCacheWithGoogleAppID:(NSString *)googleAppID
currentTimestamp:(NSTimeInterval)currentTimestamp {
NSString *settingsFilePath = self.fileManager.settingsFilePath;
NSData *data = [self.fileManager dataWithContentsOfFile:settingsFilePath];
if (!data) {
FIRCLSDebugLog(@"[Crashlytics:Settings] No settings were cached");
return;
}
NSError *error = nil;
@synchronized(self) {
_settingsDictionary = [NSJSONSerialization JSONObjectWithData:data options:0 error:&error];
}
if (!_settingsDictionary) {
FIRCLSErrorLog(@"Could not load settings file data with error: %@", error.localizedDescription);
// Attempt to remove it, in case it's messed up
[self deleteCachedSettings];
return;
}
NSDictionary<NSString *, id> *cacheKey = [self loadCacheKey];
if (!cacheKey) {
FIRCLSErrorLog(@"Could not load settings cache key");
[self deleteCachedSettings];
return;
}
NSString *cachedGoogleAppID = cacheKey[GoogleAppIDKey];
if (![cachedGoogleAppID isEqualToString:googleAppID]) {
FIRCLSDebugLog(
@"[Crashlytics:Settings] Invalidating settings cache because Google App ID changed");
[self deleteCachedSettings];
return;
}
NSTimeInterval cacheCreatedAt = [cacheKey[CreatedAtKey] unsignedIntValue];
NSTimeInterval cacheDurationSeconds = self.cacheDurationSeconds;
if (currentTimestamp > (cacheCreatedAt + cacheDurationSeconds)) {
FIRCLSDebugLog(@"[Crashlytics:Settings] Settings TTL expired");
@synchronized(self) {
self.isCacheKeyExpired = YES;
}
}
NSString *cacheBuildInstanceID = cacheKey[BuildInstanceID];
if (![cacheBuildInstanceID isEqualToString:self.appIDModel.buildInstanceID]) {
FIRCLSDebugLog(@"[Crashlytics:Settings] Settings expired because build instance changed");
@synchronized(self) {
self.isCacheKeyExpired = YES;
}
}
NSString *cacheAppVersion = cacheKey[AppVersion];
if (![cacheAppVersion isEqualToString:self.appIDModel.synthesizedVersion]) {
FIRCLSDebugLog(@"[Crashlytics:Settings] Settings expired because app version changed");
@synchronized(self) {
self.isCacheKeyExpired = YES;
}
}
}
- (void)cacheSettingsWithGoogleAppID:(NSString *)googleAppID
currentTimestamp:(NSTimeInterval)currentTimestamp {
NSNumber *createdAtTimestamp = [NSNumber numberWithDouble:currentTimestamp];
NSDictionary *cacheKey = @{
CreatedAtKey : createdAtTimestamp,
GoogleAppIDKey : googleAppID,
BuildInstanceID : self.appIDModel.buildInstanceID,
AppVersion : self.appIDModel.synthesizedVersion,
};
NSError *error = nil;
NSData *jsonData = [NSJSONSerialization dataWithJSONObject:cacheKey
options:kNilOptions
error:&error];
if (!jsonData) {
FIRCLSErrorLog(@"Could not create settings cache key with error: %@",
error.localizedDescription);
return;
}
if ([self.fileManager fileExistsAtPath:self.fileManager.settingsCacheKeyPath]) {
[self.fileManager removeItemAtPath:self.fileManager.settingsCacheKeyPath];
}
[self.fileManager createFileAtPath:self.fileManager.settingsCacheKeyPath
contents:jsonData
attributes:nil];
// If Settings were expired before, they should no longer be expired after this.
// This may be set back to YES if reloading from the cache fails
@synchronized(self) {
self.isCacheKeyExpired = NO;
}
[self reloadFromCacheWithGoogleAppID:googleAppID currentTimestamp:currentTimestamp];
}
#pragma mark - Convenience Methods
- (NSDictionary *)loadCacheKey {
NSData *cacheKeyData =
[self.fileManager dataWithContentsOfFile:self.fileManager.settingsCacheKeyPath];
if (!cacheKeyData) {
return nil;
}
NSError *error = nil;
NSDictionary *cacheKey = [NSJSONSerialization JSONObjectWithData:cacheKeyData
options:NSJSONReadingAllowFragments
error:&error];
return cacheKey;
}
- (void)deleteCachedSettings {
__weak FIRCLSSettings *weakSelf = self;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
__strong FIRCLSSettings *strongSelf = weakSelf;
if ([strongSelf.fileManager fileExistsAtPath:strongSelf.fileManager.settingsFilePath]) {
[strongSelf.fileManager removeItemAtPath:strongSelf.fileManager.settingsFilePath];
}
if ([strongSelf.fileManager fileExistsAtPath:strongSelf.fileManager.settingsCacheKeyPath]) {
[strongSelf.fileManager removeItemAtPath:strongSelf.fileManager.settingsCacheKeyPath];
}
});
@synchronized(self) {
self.isCacheKeyExpired = YES;
_settingsDictionary = nil;
}
}
- (NSDictionary<NSString *, id> *)settingsDictionary {
@synchronized(self) {
return _settingsDictionary;
}
}
#pragma mark - Settings Groups
- (NSDictionary<NSString *, id> *)appSettings {
return self.settingsDictionary[@"app"];
}
- (NSDictionary<NSString *, id> *)sessionSettings {
return self.settingsDictionary[@"session"];
}
- (NSDictionary<NSString *, id> *)featuresSettings {
return self.settingsDictionary[@"features"];
}
- (NSDictionary<NSString *, id> *)fabricSettings {
return self.settingsDictionary[@"fabric"];
}
#pragma mark - Caching
- (BOOL)isCacheExpired {
if (!self.settingsDictionary) {
return YES;
}
@synchronized(self) {
return self.isCacheKeyExpired;
}
}
- (uint32_t)cacheDurationSeconds {
id fetchedCacheDuration = self.settingsDictionary[@"cache_duration"];
if (fetchedCacheDuration) {
return [fetchedCacheDuration unsignedIntValue];
}
return 60 * 60;
}
#pragma mark - On / Off Switches
- (BOOL)errorReportingEnabled {
NSNumber *value = [self featuresSettings][@"collect_logged_exceptions"];
if (value != nil) {
return [value boolValue];
}
return YES;
}
- (BOOL)customExceptionsEnabled {
// Right now, recording custom exceptions from the API and
// automatically capturing non-fatal errors go hand in hand
return [self errorReportingEnabled];
}
- (BOOL)collectReportsEnabled {
NSNumber *value = [self featuresSettings][@"collect_reports"];
if (value != nil) {
return value.boolValue;
}
return YES;
}
- (BOOL)metricKitCollectionEnabled {
NSNumber *value = [self featuresSettings][@"collect_metric_kit"];
if (value != nil) {
return value.boolValue;
}
return NO;
}
#pragma mark - Optional Limit Overrides
- (uint32_t)errorLogBufferSize {
return [self logBufferSize];
}
- (uint32_t)logBufferSize {
NSNumber *value = [self sessionSettings][@"log_buffer_size"];
if (value != nil) {
return value.unsignedIntValue;
}
return 64 * 1000;
}
- (uint32_t)maxCustomExceptions {
NSNumber *value = [self sessionSettings][@"max_custom_exception_events"];
if (value != nil) {
return value.unsignedIntValue;
}
return 8;
}
- (uint32_t)maxCustomKeys {
NSNumber *value = [self sessionSettings][@"max_custom_key_value_pairs"];
if (value != nil) {
return value.unsignedIntValue;
}
return 64;
}
#pragma mark - On Demand Reporting Parameters
- (double)onDemandUploadRate {
NSNumber *value = self.settingsDictionary[@"on_demand_upload_rate_per_minute"];
if (value != nil) {
return value.doubleValue;
}
return 10; // on-demand uploads allowed per minute
}
- (double)onDemandBackoffBase {
NSNumber *value = self.settingsDictionary[@"on_demand_backoff_base"];
if (value != nil) {
return [value doubleValue];
}
return 1.5; // base of exponent for exponential backoff
}
- (uint32_t)onDemandBackoffStepDuration {
NSNumber *value = self.settingsDictionary[@"on_demand_backoff_step_duration_seconds"];
if (value != nil) {
return value.unsignedIntValue;
}
return 6; // step duration for exponential backoff
}
@end