SFAnalytics.m
1 /* 2 * Copyright (c) 2017 Apple Inc. All Rights Reserved. 3 * 4 * @APPLE_LICENSE_HEADER_START@ 5 * 6 * This file contains Original Code and/or Modifications of Original Code 7 * as defined in and that are subject to the Apple Public Source License 8 * Version 2.0 (the 'License'). You may not use this file except in 9 * compliance with the License. Please obtain a copy of the License at 10 * http://www.opensource.apple.com/apsl/ and read it before using this 11 * file. 12 * 13 * The Original Code and all software distributed under the License are 14 * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER 15 * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES, 16 * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY, 17 * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT. 18 * Please see the License for the specific language governing rights and 19 * limitations under the License. 20 * 21 * @APPLE_LICENSE_HEADER_END@ 22 */ 23 24 #if __OBJC2__ 25 26 #import "SFAnalytics+Internal.h" 27 #import "SFAnalyticsDefines.h" 28 #import "SFAnalyticsActivityTracker+Internal.h" 29 #import "SFAnalyticsSampler+Internal.h" 30 #import "SFAnalyticsMultiSampler+Internal.h" 31 #import "SFAnalyticsSQLiteStore.h" 32 #import "NSDate+SFAnalytics.h" 33 #import "utilities/debugging.h" 34 #import <utilities/SecFileLocations.h> 35 #import <objc/runtime.h> 36 #import <sys/stat.h> 37 #import <CoreFoundation/CFPriv.h> 38 #include <os/transaction_private.h> 39 #include <os/variant_private.h> 40 41 #import <utilities/SecCoreAnalytics.h> 42 43 #if TARGET_OS_OSX 44 #include <sys/sysctl.h> 45 #include <membership.h> 46 #else 47 #import <sys/utsname.h> 48 #endif 49 50 // SFAnalyticsDefines constants 51 NSString* const SFAnalyticsTableSuccessCount = @"success_count"; 52 NSString* const SFAnalyticsTableHardFailures = @"hard_failures"; 53 NSString* const SFAnalyticsTableSoftFailures = @"soft_failures"; 54 NSString* const SFAnalyticsTableSamples = @"samples"; 55 NSString* const SFAnalyticsTableNotes = @"notes"; 56 57 NSString* const SFAnalyticsColumnSuccessCount = @"success_count"; 58 NSString* const SFAnalyticsColumnHardFailureCount = @"hard_failure_count"; 59 NSString* const SFAnalyticsColumnSoftFailureCount = @"soft_failure_count"; 60 NSString* const SFAnalyticsColumnSampleValue = @"value"; 61 NSString* const SFAnalyticsColumnSampleName = @"name"; 62 63 NSString* const SFAnalyticsPostTime = @"postTime"; 64 NSString* const SFAnalyticsEventTime = @"eventTime"; 65 NSString* const SFAnalyticsEventType = @"eventType"; 66 NSString* const SFAnalyticsEventTypeErrorEvent = @"errorEvent"; 67 NSString* const SFAnalyticsEventErrorDestription = @"errorDescription"; 68 NSString* const SFAnalyticsEventClassKey = @"eventClass"; 69 70 NSString* const SFAnalyticsAttributeErrorUnderlyingChain = @"errorChain"; 71 NSString* const SFAnalyticsAttributeErrorDomain = @"errorDomain"; 72 NSString* const SFAnalyticsAttributeErrorCode = @"errorCode"; 73 74 NSString* const SFAnalyticsAttributeLastUploadTime = @"lastUploadTime"; 75 76 NSString* const SFAnalyticsUserDefaultsSuite = @"com.apple.security.analytics"; 77 78 char* const SFAnalyticsFireSamplersNotification = "com.apple.security.sfanalytics.samplers"; 79 80 NSString* const SFAnalyticsTopicCloudServices = @"CloudServicesTopic"; 81 NSString* const SFAnalyticsTopicKeySync = @"KeySyncTopic"; 82 NSString* const SFAnalyticsTopicTrust = @"TrustTopic"; 83 NSString* const SFAnalyticsTopicTransparency = @"TransparencyTopic"; 84 NSString* const SFAnalyticsTopicNetworking = @"NetworkingTopic"; 85 86 NSString* const SFAnalyticsTableSchema = @"CREATE TABLE IF NOT EXISTS hard_failures (\n" 87 @"id INTEGER PRIMARY KEY AUTOINCREMENT,\n" 88 @"timestamp REAL," 89 @"data BLOB\n" 90 @");\n" 91 @"DROP TRIGGER IF EXISTS maintain_ring_buffer_hard_failures;\n" 92 @"CREATE TRIGGER IF NOT EXISTS maintain_ring_buffer_hard_failures_v2 AFTER INSERT ON hard_failures\n" 93 @"BEGIN\n" 94 @"DELETE FROM hard_failures WHERE id <= NEW.id - 1000;\n" 95 @"END;\n" 96 @"CREATE TABLE IF NOT EXISTS soft_failures (\n" 97 @"id INTEGER PRIMARY KEY AUTOINCREMENT,\n" 98 @"timestamp REAL," 99 @"data BLOB\n" 100 @");\n" 101 @"DROP TRIGGER IF EXISTS maintain_ring_buffer_soft_failures;\n" 102 @"CREATE TRIGGER IF NOT EXISTS maintain_ring_buffer_soft_failures_v2 AFTER INSERT ON soft_failures\n" 103 @"BEGIN\n" 104 @"DELETE FROM soft_failures WHERE id <= NEW.id - 1000;\n" 105 @"END;\n" 106 @"CREATE TABLE IF NOT EXISTS notes (\n" 107 @"id INTEGER PRIMARY KEY AUTOINCREMENT,\n" 108 @"timestamp REAL," 109 @"data BLOB\n" 110 @");\n" 111 @"DROP TRIGGER IF EXISTS maintain_ring_buffer_notes;\n" 112 @"CREATE TRIGGER IF NOT EXISTS maintain_ring_buffer_notes_v2 AFTER INSERT ON notes\n" 113 @"BEGIN\n" 114 @"DELETE FROM notes WHERE id <= NEW.id - 1000;\n" 115 @"END;\n" 116 @"CREATE TABLE IF NOT EXISTS samples (\n" 117 @"id INTEGER PRIMARY KEY AUTOINCREMENT,\n" 118 @"timestamp REAL,\n" 119 @"name STRING,\n" 120 @"value REAL\n" 121 @");\n" 122 @"DROP TRIGGER IF EXISTS maintain_ring_buffer_samples;\n" 123 @"CREATE TRIGGER IF NOT EXISTS maintain_ring_buffer_samples_v2 AFTER INSERT ON samples\n" 124 @"BEGIN\n" 125 @"DELETE FROM samples WHERE id <= NEW.id - 1000;\n" 126 @"END;\n" 127 @"CREATE TABLE IF NOT EXISTS success_count (\n" 128 @"event_type STRING PRIMARY KEY,\n" 129 @"success_count INTEGER,\n" 130 @"hard_failure_count INTEGER,\n" 131 @"soft_failure_count INTEGER\n" 132 @");\n" 133 @"DROP TABLE IF EXISTS all_events;\n"; 134 135 NSUInteger const SFAnalyticsMaxEventsToReport = 1000; 136 137 NSString* const SFAnalyticsErrorDomain = @"com.apple.security.sfanalytics"; 138 139 // Local constants 140 NSString* const SFAnalyticsEventBuild = @"build"; 141 NSString* const SFAnalyticsEventProduct = @"product"; 142 NSString* const SFAnalyticsEventModelID = @"modelid"; 143 NSString* const SFAnalyticsEventInternal = @"internal"; 144 const NSTimeInterval SFAnalyticsSamplerIntervalOncePerReport = -1.0; 145 146 @interface SFAnalytics () 147 @property (nonatomic) SFAnalyticsSQLiteStore* database; 148 @property (nonatomic) dispatch_queue_t queue; 149 @end 150 151 @implementation SFAnalytics { 152 SFAnalyticsSQLiteStore* _database; 153 dispatch_queue_t _queue; 154 NSMutableDictionary<NSString*, SFAnalyticsSampler*>* _samplers; 155 NSMutableDictionary<NSString*, SFAnalyticsMultiSampler*>* _multisamplers; 156 unsigned int _disableLogging:1; 157 } 158 159 + (instancetype)logger 160 { 161 if (self == [SFAnalytics class]) { 162 secerror("attempt to instatiate abstract class SFAnalytics"); 163 return nil; 164 } 165 166 SFAnalytics* logger = nil; 167 @synchronized(self) { 168 logger = objc_getAssociatedObject(self, "SFAnalyticsInstance"); 169 if (!logger) { 170 logger = [[self alloc] init]; 171 objc_setAssociatedObject(self, "SFAnalyticsInstance", logger, OBJC_ASSOCIATION_RETAIN); 172 } 173 } 174 175 [logger database]; // For unit testing so there's always a database. DB shouldn't be nilled in production though 176 return logger; 177 } 178 179 + (NSString*)databasePath 180 { 181 return nil; 182 } 183 184 + (NSString *)defaultAnalyticsDatabasePath:(NSString *)basename 185 { 186 WithPathInKeychainDirectory(CFSTR("Analytics"), ^(const char *path) { 187 #if TARGET_OS_IPHONE 188 /* We need _securityd, _trustd, and root all to be able to write. They share no groups. */ 189 mode_t permissions = 0777; 190 #else 191 mode_t permissions = 0700; 192 #endif // TARGET_OS_IPHONE 193 int ret = mkpath_np(path, permissions); 194 if (!(ret == 0 || ret == EEXIST)) { 195 secerror("could not create path: %s (%s)", path, strerror(ret)); 196 } 197 chmod(path, permissions); 198 }); 199 NSString *path = [NSString stringWithFormat:@"Analytics/%@.db", basename]; 200 return [(__bridge_transfer NSURL*)SecCopyURLForFileInKeychainDirectory((__bridge CFStringRef)path) path]; 201 } 202 203 + (NSString *)defaultProtectedAnalyticsDatabasePath:(NSString *)basename uuid:(NSUUID * __nullable)userUuid 204 { 205 // Create the top-level directory with full access 206 NSMutableString *directory = [NSMutableString stringWithString:@"sfanalytics"]; 207 WithPathInProtectedDirectory((__bridge CFStringRef)directory, ^(const char *path) { 208 mode_t permissions = 0777; 209 int ret = mkpath_np(path, permissions); 210 if (!(ret == 0 || ret == EEXIST)) { 211 secerror("could not create path: %s (%s)", path, strerror(ret)); 212 } 213 chmod(path, permissions); 214 }); 215 216 // create per-user directory 217 if (userUuid) { 218 [directory appendString:@"/"]; 219 [directory appendString:[userUuid UUIDString]]; 220 WithPathInProtectedDirectory((__bridge CFStringRef)directory, ^(const char *path) { 221 #if TARGET_OS_IPHONE 222 /* We need _securityd, _trustd, and root all to be able to write. They share no groups. */ 223 mode_t permissions = 0777; 224 #else 225 mode_t permissions = 0700; 226 if (geteuid() == 0 || geteuid() == 282) { 227 // Root/_trustd user directory needs to be read/write for group so that user supd can upload system data 228 permissions = 0775; 229 } 230 #endif // TARGET_OS_IPHONE 231 int ret = mkpath_np(path, permissions); 232 if (!(ret == 0 || ret == EEXIST)) { 233 secerror("could not create path: %s (%s)", path, strerror(ret)); 234 } 235 chmod(path, permissions); 236 }); 237 } 238 NSString *path = [NSString stringWithFormat:@"%@/%@.db", directory, basename]; 239 return [(__bridge_transfer NSURL*)SecCopyURLForFileInProtectedDirectory((__bridge CFStringRef)path) path]; 240 } 241 242 + (NSString *)defaultProtectedAnalyticsDatabasePath:(NSString *)basename 243 { 244 #if TARGET_OS_OSX 245 uid_t euid = geteuid(); 246 uuid_t currentUserUuid; 247 int ret = mbr_uid_to_uuid(euid, currentUserUuid); 248 if (ret != 0) { 249 secerror("failed to get UUID for user(%d) - %d", euid, ret); 250 return [SFAnalytics defaultProtectedAnalyticsDatabasePath:basename uuid:nil]; 251 } 252 NSUUID *userUuid = [[NSUUID alloc] initWithUUIDBytes:currentUserUuid]; 253 return [SFAnalytics defaultProtectedAnalyticsDatabasePath:basename uuid:userUuid]; 254 #else 255 return [SFAnalytics defaultProtectedAnalyticsDatabasePath:basename uuid:nil]; 256 #endif // TARGET_OS_IPHONE 257 } 258 259 + (NSInteger)fuzzyDaysSinceDate:(NSDate*)date 260 { 261 // Sentinel: it didn't happen at all 262 if (!date) { 263 return -1; 264 } 265 266 // Sentinel: it happened but we don't know when because the date doesn't make sense 267 // Magic number represents January 1, 2017. 268 if ([date compare:[NSDate dateWithTimeIntervalSince1970:1483228800]] == NSOrderedAscending) { 269 return 1000; 270 } 271 272 NSInteger secondsPerDay = 60 * 60 * 24; 273 274 NSTimeInterval timeIntervalSinceDate = [[NSDate date] timeIntervalSinceDate:date]; 275 if (timeIntervalSinceDate < secondsPerDay) { 276 return 0; 277 } 278 else if (timeIntervalSinceDate < (secondsPerDay * 7)) { 279 return 1; 280 } 281 else if (timeIntervalSinceDate < (secondsPerDay * 30)) { 282 return 7; 283 } 284 else if (timeIntervalSinceDate < (secondsPerDay * 365)) { 285 return 30; 286 } 287 else { 288 return 365; 289 } 290 } 291 292 + (NSInteger)fuzzyInteger:(NSInteger)num 293 { 294 NSInteger sign = 1; 295 if(num < 0) { 296 sign = -1; 297 num = -num; 298 } 299 300 // Differentiate zero and non-zero.... 301 if(num == 0) { 302 return 0; 303 } 304 305 if(num <= 5) { 306 return sign*5; 307 } 308 309 // Otherwise, round to the nearest five 310 NSInteger mod = num % 5; 311 312 if(mod <= 2) { 313 return sign*(num - mod); 314 } else { 315 return sign*(num + (5-mod)); 316 } 317 } 318 319 + (NSNumber*)fuzzyNumber:(NSNumber*)num 320 { 321 return [NSNumber numberWithInteger:[self fuzzyInteger:[num integerValue]]]; 322 } 323 324 // Instantiate lazily so unit tests can have clean databases each 325 - (SFAnalyticsSQLiteStore*)database 326 { 327 if (!_database) { 328 _database = [SFAnalyticsSQLiteStore storeWithPath:self.class.databasePath schema:SFAnalyticsTableSchema]; 329 if (!_database) { 330 seccritical("Did not get a database! (Client %@)", NSStringFromClass([self class])); 331 } 332 } 333 return _database; 334 } 335 336 - (void)removeState 337 { 338 [_samplers removeAllObjects]; 339 [_multisamplers removeAllObjects]; 340 341 __weak __typeof(self) weakSelf = self; 342 dispatch_sync(_queue, ^{ 343 __strong __typeof(self) strongSelf = weakSelf; 344 if (strongSelf) { 345 [strongSelf.database close]; 346 strongSelf->_database = nil; 347 } 348 }); 349 } 350 351 - (void)setDateProperty:(NSDate*)date forKey:(NSString*)key 352 { 353 __weak __typeof(self) weakSelf = self; 354 dispatch_sync(_queue, ^{ 355 __strong __typeof(self) strongSelf = weakSelf; 356 if (strongSelf) { 357 [strongSelf.database setDateProperty:date forKey:key]; 358 } 359 }); 360 } 361 362 - (NSDate*)datePropertyForKey:(NSString*)key 363 { 364 __block NSDate* result = nil; 365 __weak __typeof(self) weakSelf = self; 366 dispatch_sync(_queue, ^{ 367 __strong __typeof(self) strongSelf = weakSelf; 368 if (strongSelf) { 369 result = [strongSelf.database datePropertyForKey:key]; 370 } 371 }); 372 return result; 373 } 374 375 376 - (void)incrementIntegerPropertyForKey:(NSString*)key 377 { 378 __weak __typeof(self) weakSelf = self; 379 dispatch_sync(_queue, ^{ 380 __strong __typeof(self) strongSelf = weakSelf; 381 if (strongSelf == nil) { 382 return; 383 } 384 NSInteger integer = [[strongSelf.database propertyForKey:key] integerValue]; 385 [strongSelf.database setProperty:[NSString stringWithFormat:@"%ld", (long)integer + 1] forKey:key]; 386 }); 387 } 388 389 - (void)setNumberProperty:(NSNumber* _Nullable)number forKey:(NSString*)key 390 { 391 __weak __typeof(self) weakSelf = self; 392 dispatch_sync(_queue, ^{ 393 __strong __typeof(self) strongSelf = weakSelf; 394 if (strongSelf) { 395 [strongSelf.database setProperty:[number stringValue] forKey:key]; 396 } 397 }); 398 } 399 400 - (NSNumber* _Nullable)numberPropertyForKey:(NSString*)key 401 { 402 __block NSNumber* result = nil; 403 __weak __typeof(self) weakSelf = self; 404 dispatch_sync(_queue, ^{ 405 __strong __typeof(self) strongSelf = weakSelf; 406 if (strongSelf) { 407 NSString *property = [strongSelf.database propertyForKey:key]; 408 if (property) { 409 result = [NSNumber numberWithInteger:[property integerValue]]; 410 } 411 } 412 }); 413 return result; 414 } 415 416 + (NSString*)hwModelID 417 { 418 static NSString *hwModel = nil; 419 static dispatch_once_t onceToken; 420 dispatch_once(&onceToken, ^{ 421 #if TARGET_OS_SIMULATOR 422 // Asking for a real value in the simulator gives the results for the underlying mac. Not particularly useful. 423 hwModel = [NSString stringWithFormat:@"%s", getenv("SIMULATOR_MODEL_IDENTIFIER")]; 424 #elif TARGET_OS_OSX 425 size_t size; 426 sysctlbyname("hw.model", NULL, &size, NULL, 0); 427 char *sysctlString = malloc(size); 428 sysctlbyname("hw.model", sysctlString, &size, NULL, 0); 429 hwModel = [[NSString alloc] initWithUTF8String:sysctlString]; 430 free(sysctlString); 431 #else 432 struct utsname systemInfo; 433 uname(&systemInfo); 434 435 hwModel = [NSString stringWithCString:systemInfo.machine 436 encoding:NSUTF8StringEncoding]; 437 #endif 438 }); 439 return hwModel; 440 } 441 442 + (void)addOSVersionToEvent:(NSMutableDictionary*)eventDict { 443 static dispatch_once_t onceToken; 444 static NSString *build = NULL; 445 static NSString *product = NULL; 446 static NSString *modelID = nil; 447 static BOOL internal = NO; 448 dispatch_once(&onceToken, ^{ 449 NSDictionary *version = CFBridgingRelease(_CFCopySystemVersionDictionary()); 450 if (version == NULL) 451 return; 452 build = version[(__bridge NSString *)_kCFSystemVersionBuildVersionKey]; 453 product = version[(__bridge NSString *)_kCFSystemVersionProductNameKey]; 454 internal = os_variant_has_internal_diagnostics("com.apple.security"); 455 456 modelID = [self hwModelID]; 457 }); 458 if (build) { 459 eventDict[SFAnalyticsEventBuild] = build; 460 } 461 if (product) { 462 eventDict[SFAnalyticsEventProduct] = product; 463 } 464 if (modelID) { 465 eventDict[SFAnalyticsEventModelID] = modelID; 466 } 467 if (internal) { 468 eventDict[SFAnalyticsEventInternal] = @YES; 469 } 470 } 471 472 - (instancetype)init 473 { 474 if (self = [super init]) { 475 _queue = dispatch_queue_create("SFAnalytics data access queue", DISPATCH_QUEUE_SERIAL_WITH_AUTORELEASE_POOL); 476 _samplers = [NSMutableDictionary<NSString*, SFAnalyticsSampler*> new]; 477 _multisamplers = [NSMutableDictionary<NSString*, SFAnalyticsMultiSampler*> new]; 478 [self database]; // for side effect of instantiating DB object. Used for testing. 479 } 480 481 return self; 482 } 483 484 - (NSDictionary *)coreAnalyticsKeyFilter:(NSDictionary<NSString *, id> *)info 485 { 486 NSMutableDictionary *filtered = [NSMutableDictionary dictionary]; 487 [info enumerateKeysAndObjectsUsingBlock:^(NSString *_Nonnull key, id _Nonnull obj, BOOL * _Nonnull stop) { 488 filtered[[key stringByReplacingOccurrencesOfString:@"-" withString:@"_"]] = obj; 489 }]; 490 return filtered; 491 } 492 493 // Daily CoreAnalytics metrics 494 // Call this once per say if you want to have the once per day sampler collect their data and submit it 495 496 - (void)dailyCoreAnalyticsMetrics:(NSString *)eventName 497 { 498 NSMutableDictionary<NSString*, NSNumber*> *dailyMetrics = [NSMutableDictionary dictionary]; 499 __block NSDictionary<NSString*, SFAnalyticsMultiSampler*>* multisamplers; 500 __block NSDictionary<NSString*, SFAnalyticsSampler*>* samplers; 501 502 dispatch_sync(_queue, ^{ 503 multisamplers = [self->_multisamplers copy]; 504 samplers = [self->_samplers copy]; 505 }); 506 507 [multisamplers enumerateKeysAndObjectsUsingBlock:^(NSString * _Nonnull key, SFAnalyticsMultiSampler * _Nonnull obj, BOOL * _Nonnull stop) { 508 if (obj.oncePerReport == FALSE) { 509 return; 510 } 511 NSDictionary<NSString*, NSNumber*> *samples = [obj sampleNow]; 512 if (samples == nil) { 513 return; 514 } 515 [dailyMetrics addEntriesFromDictionary:samples]; 516 }]; 517 518 [samplers enumerateKeysAndObjectsUsingBlock:^(NSString * _Nonnull key, SFAnalyticsSampler * _Nonnull obj, BOOL * _Nonnull stop) { 519 if (obj.oncePerReport == FALSE) { 520 return; 521 } 522 dailyMetrics[key] = [obj sampleNow]; 523 }]; 524 525 [SecCoreAnalytics sendEvent:eventName event:[self coreAnalyticsKeyFilter:dailyMetrics]]; 526 } 527 528 // MARK: Event logging 529 530 - (void)logSuccessForEventNamed:(NSString*)eventName timestampBucket:(SFAnalyticsTimestampBucket)timestampBucket 531 { 532 [self logEventNamed:eventName class:SFAnalyticsEventClassSuccess attributes:nil timestampBucket:timestampBucket]; 533 } 534 535 - (void)logSuccessForEventNamed:(NSString*)eventName 536 { 537 [self logSuccessForEventNamed:eventName timestampBucket:SFAnalyticsTimestampBucketSecond]; 538 } 539 540 - (void)logHardFailureForEventNamed:(NSString*)eventName withAttributes:(NSDictionary*)attributes timestampBucket:(SFAnalyticsTimestampBucket)timestampBucket 541 { 542 [self logEventNamed:eventName class:SFAnalyticsEventClassHardFailure attributes:attributes timestampBucket:timestampBucket]; 543 } 544 545 - (void)logHardFailureForEventNamed:(NSString*)eventName withAttributes:(NSDictionary*)attributes 546 { 547 [self logHardFailureForEventNamed:eventName withAttributes:attributes timestampBucket:SFAnalyticsTimestampBucketSecond]; 548 } 549 550 - (void)logSoftFailureForEventNamed:(NSString*)eventName withAttributes:(NSDictionary*)attributes timestampBucket:(SFAnalyticsTimestampBucket)timestampBucket 551 { 552 [self logEventNamed:eventName class:SFAnalyticsEventClassSoftFailure attributes:attributes timestampBucket:timestampBucket]; 553 } 554 555 - (void)logSoftFailureForEventNamed:(NSString*)eventName withAttributes:(NSDictionary*)attributes 556 { 557 [self logSoftFailureForEventNamed:eventName withAttributes:attributes timestampBucket:SFAnalyticsTimestampBucketSecond]; 558 } 559 560 - (void)logResultForEvent:(NSString*)eventName hardFailure:(bool)hardFailure result:(NSError*)eventResultError timestampBucket:(SFAnalyticsTimestampBucket)timestampBucket 561 { 562 [self logResultForEvent:eventName hardFailure:hardFailure result:eventResultError withAttributes:nil timestampBucket:SFAnalyticsTimestampBucketSecond]; 563 } 564 565 - (void)logResultForEvent:(NSString*)eventName hardFailure:(bool)hardFailure result:(NSError*)eventResultError 566 { 567 [self logResultForEvent:eventName hardFailure:hardFailure result:eventResultError timestampBucket:SFAnalyticsTimestampBucketSecond]; 568 } 569 570 - (void)logResultForEvent:(NSString*)eventName hardFailure:(bool)hardFailure result:(NSError*)eventResultError withAttributes:(NSDictionary*)attributes timestampBucket:(SFAnalyticsTimestampBucket)timestampBucket 571 { 572 if(!eventResultError) { 573 [self logSuccessForEventNamed:eventName]; 574 } else { 575 // Make an Attributes dictionary 576 NSMutableDictionary* eventAttributes = nil; 577 if (attributes) { 578 eventAttributes = [attributes mutableCopy]; 579 } else { 580 eventAttributes = [NSMutableDictionary dictionary]; 581 } 582 583 /* if we have underlying errors, capture the chain below the top-most error */ 584 NSError *underlyingError = eventResultError.userInfo[NSUnderlyingErrorKey]; 585 if ([underlyingError isKindOfClass:[NSError class]]) { 586 NSMutableString *chain = [NSMutableString string]; 587 int count = 0; 588 do { 589 [chain appendFormat:@"%@-%ld:", underlyingError.domain, (long)underlyingError.code]; 590 underlyingError = underlyingError.userInfo[NSUnderlyingErrorKey]; 591 } while (count++ < 5 && [underlyingError isKindOfClass:[NSError class]]); 592 593 eventAttributes[SFAnalyticsAttributeErrorUnderlyingChain] = chain; 594 } 595 596 eventAttributes[SFAnalyticsAttributeErrorDomain] = eventResultError.domain; 597 eventAttributes[SFAnalyticsAttributeErrorCode] = @(eventResultError.code); 598 599 if(hardFailure) { 600 [self logHardFailureForEventNamed:eventName withAttributes:eventAttributes]; 601 } else { 602 [self logSoftFailureForEventNamed:eventName withAttributes:eventAttributes]; 603 } 604 } 605 } 606 607 - (void)logResultForEvent:(NSString*)eventName hardFailure:(bool)hardFailure result:(NSError*)eventResultError withAttributes:(NSDictionary*)attributes 608 { 609 [self logResultForEvent:eventName hardFailure:hardFailure result:eventResultError withAttributes:attributes timestampBucket:SFAnalyticsTimestampBucketSecond]; 610 } 611 612 - (void)noteEventNamed:(NSString*)eventName timestampBucket:(SFAnalyticsTimestampBucket)timestampBucket 613 { 614 [self logEventNamed:eventName class:SFAnalyticsEventClassNote attributes:nil timestampBucket:timestampBucket]; 615 } 616 617 - (void)noteEventNamed:(NSString*)eventName 618 { 619 [self noteEventNamed:eventName timestampBucket:SFAnalyticsTimestampBucketSecond]; 620 } 621 622 - (void)logEventNamed:(NSString*)eventName class:(SFAnalyticsEventClass)class attributes:(NSDictionary*)attributes timestampBucket:(SFAnalyticsTimestampBucket)timestampBucket 623 { 624 if (!eventName) { 625 secerror("SFAnalytics: attempt to log an event with no name"); 626 return; 627 } 628 629 __weak __typeof(self) weakSelf = self; 630 dispatch_sync(_queue, ^{ 631 __strong __typeof(self) strongSelf = weakSelf; 632 if (!strongSelf || strongSelf->_disableLogging) { 633 return; 634 } 635 636 [strongSelf.database begin]; 637 638 NSDictionary* eventDict = [self eventDictForEventName:eventName withAttributes:attributes eventClass:class timestampBucket:timestampBucket]; 639 640 if (class == SFAnalyticsEventClassHardFailure) { 641 [strongSelf.database addEventDict:eventDict toTable:SFAnalyticsTableHardFailures timestampBucket:timestampBucket]; 642 [strongSelf.database incrementHardFailureCountForEventType:eventName]; 643 } 644 else if (class == SFAnalyticsEventClassSoftFailure) { 645 [strongSelf.database addEventDict:eventDict toTable:SFAnalyticsTableSoftFailures timestampBucket:timestampBucket]; 646 [strongSelf.database incrementSoftFailureCountForEventType:eventName]; 647 } 648 else if (class == SFAnalyticsEventClassNote) { 649 [strongSelf.database addEventDict:eventDict toTable:SFAnalyticsTableNotes timestampBucket:timestampBucket]; 650 [strongSelf.database incrementSuccessCountForEventType:eventName]; 651 } 652 else if (class == SFAnalyticsEventClassSuccess) { 653 [strongSelf.database incrementSuccessCountForEventType:eventName]; 654 } 655 656 [strongSelf.database end]; 657 }); 658 } 659 660 - (void)logEventNamed:(NSString*)eventName class:(SFAnalyticsEventClass)class attributes:(NSDictionary*)attributes 661 { 662 [self logEventNamed:eventName class:class attributes:attributes timestampBucket:SFAnalyticsTimestampBucketSecond]; 663 } 664 665 - (NSDictionary*) eventDictForEventName:(NSString*)eventName withAttributes:(NSDictionary*)attributes eventClass:(SFAnalyticsEventClass)eventClass timestampBucket:(NSTimeInterval)timestampBucket 666 { 667 NSMutableDictionary* eventDict = attributes ? attributes.mutableCopy : [NSMutableDictionary dictionary]; 668 eventDict[SFAnalyticsEventType] = eventName; 669 670 NSTimeInterval timestamp = [[NSDate date] timeIntervalSince1970WithBucket:timestampBucket]; 671 672 // our backend wants timestamps in milliseconds 673 eventDict[SFAnalyticsEventTime] = @(timestamp * 1000); 674 eventDict[SFAnalyticsEventClassKey] = @(eventClass); 675 [SFAnalytics addOSVersionToEvent:eventDict]; 676 677 return eventDict; 678 } 679 680 // MARK: Sampling 681 682 - (SFAnalyticsSampler*)addMetricSamplerForName:(NSString *)samplerName withTimeInterval:(NSTimeInterval)timeInterval block:(NSNumber *(^)(void))block 683 { 684 if (!samplerName) { 685 secerror("SFAnalytics: cannot add sampler without name"); 686 return nil; 687 } 688 if (timeInterval < 1.0f && timeInterval != SFAnalyticsSamplerIntervalOncePerReport) { 689 secerror("SFAnalytics: cannot add sampler with interval %f", timeInterval); 690 return nil; 691 } 692 if (!block) { 693 secerror("SFAnalytics: cannot add sampler without block"); 694 return nil; 695 } 696 697 __block SFAnalyticsSampler* sampler = nil; 698 699 __weak __typeof(self) weakSelf = self; 700 dispatch_sync(_queue, ^{ 701 __strong __typeof(self) strongSelf = weakSelf; 702 if (strongSelf->_samplers[samplerName]) { 703 secerror("SFAnalytics: sampler \"%@\" already exists", samplerName); 704 } else { 705 sampler = [[SFAnalyticsSampler alloc] initWithName:samplerName interval:timeInterval block:block clientClass:[self class]]; 706 strongSelf->_samplers[samplerName] = sampler; // If sampler did not init because of bad data this 'removes' it from the dict, so a noop 707 } 708 }); 709 710 return sampler; 711 } 712 713 - (SFAnalyticsMultiSampler*)AddMultiSamplerForName:(NSString *)samplerName withTimeInterval:(NSTimeInterval)timeInterval block:(NSDictionary<NSString *,NSNumber *> *(^)(void))block 714 { 715 if (!samplerName) { 716 secerror("SFAnalytics: cannot add sampler without name"); 717 return nil; 718 } 719 if (timeInterval < 1.0f && timeInterval != SFAnalyticsSamplerIntervalOncePerReport) { 720 secerror("SFAnalytics: cannot add sampler with interval %f", timeInterval); 721 return nil; 722 } 723 if (!block) { 724 secerror("SFAnalytics: cannot add sampler without block"); 725 return nil; 726 } 727 728 __block SFAnalyticsMultiSampler* sampler = nil; 729 __weak __typeof(self) weakSelf = self; 730 dispatch_sync(_queue, ^{ 731 __strong __typeof(self) strongSelf = weakSelf; 732 if (strongSelf->_multisamplers[samplerName]) { 733 secerror("SFAnalytics: multisampler \"%@\" already exists", samplerName); 734 } else { 735 sampler = [[SFAnalyticsMultiSampler alloc] initWithName:samplerName interval:timeInterval block:block clientClass:[self class]]; 736 strongSelf->_multisamplers[samplerName] = sampler; 737 } 738 739 }); 740 741 return sampler; 742 } 743 744 - (SFAnalyticsSampler*)existingMetricSamplerForName:(NSString *)samplerName 745 { 746 __block SFAnalyticsSampler* sampler = nil; 747 748 __weak __typeof(self) weakSelf = self; 749 dispatch_sync(_queue, ^{ 750 __strong __typeof(self) strongSelf = weakSelf; 751 if (strongSelf) { 752 sampler = strongSelf->_samplers[samplerName]; 753 } 754 }); 755 return sampler; 756 } 757 758 - (SFAnalyticsMultiSampler*)existingMultiSamplerForName:(NSString *)samplerName 759 { 760 __block SFAnalyticsMultiSampler* sampler = nil; 761 762 __weak __typeof(self) weakSelf = self; 763 dispatch_sync(_queue, ^{ 764 __strong __typeof(self) strongSelf = weakSelf; 765 if (strongSelf) { 766 sampler = strongSelf->_multisamplers[samplerName]; 767 } 768 }); 769 return sampler; 770 } 771 772 - (void)removeMetricSamplerForName:(NSString *)samplerName 773 { 774 if (!samplerName) { 775 secerror("Attempt to remove sampler without specifying samplerName"); 776 return; 777 } 778 779 __weak __typeof(self) weakSelf = self; 780 dispatch_async(_queue, ^{ 781 os_transaction_t transaction = os_transaction_create("com.apple.security.sfanalytics.samplerGC"); 782 __strong __typeof(self) strongSelf = weakSelf; 783 if (strongSelf) { 784 [strongSelf->_samplers[samplerName] pauseSampling]; // when dealloced it would also stop, but we're not sure when that is so let's stop it right away 785 [strongSelf->_samplers removeObjectForKey:samplerName]; 786 } 787 (void)transaction; 788 transaction = nil; 789 }); 790 } 791 792 - (void)removeMultiSamplerForName:(NSString *)samplerName 793 { 794 if (!samplerName) { 795 secerror("Attempt to remove multisampler without specifying samplerName"); 796 return; 797 } 798 799 __weak __typeof(self) weakSelf = self; 800 dispatch_async(_queue, ^{ 801 os_transaction_t transaction = os_transaction_create("com.apple.security.sfanalytics.samplerGC"); 802 __strong __typeof(self) strongSelf = weakSelf; 803 if (strongSelf) { 804 [strongSelf->_multisamplers[samplerName] pauseSampling]; // when dealloced it would also stop, but we're not sure when that is so let's stop it right away 805 [strongSelf->_multisamplers removeObjectForKey:samplerName]; 806 } 807 (void)transaction; 808 transaction = nil; 809 }); 810 } 811 812 - (SFAnalyticsActivityTracker*)logSystemMetricsForActivityNamed:(NSString *)eventName withAction:(void (^)(void))action 813 { 814 if (![eventName isKindOfClass:[NSString class]]) { 815 secerror("Cannot log system metrics without name"); 816 return nil; 817 } 818 SFAnalyticsActivityTracker* tracker = [[SFAnalyticsActivityTracker alloc] initWithName:eventName clientClass:[self class]]; 819 if (action) { 820 [tracker performAction:action]; 821 } 822 return tracker; 823 } 824 825 - (SFAnalyticsActivityTracker*)startLogSystemMetricsForActivityNamed:(NSString *)eventName 826 { 827 if (![eventName isKindOfClass:[NSString class]]) { 828 secerror("Cannot log system metrics without name"); 829 return nil; 830 } 831 SFAnalyticsActivityTracker* tracker = [[SFAnalyticsActivityTracker alloc] initWithName:eventName clientClass:[self class]]; 832 [tracker start]; 833 return tracker; 834 } 835 836 - (void)logMetric:(NSNumber *)metric withName:(NSString *)metricName 837 { 838 [self logMetric:metric withName:metricName oncePerReport:NO]; 839 } 840 841 - (void)logMetric:(NSNumber*)metric withName:(NSString*)metricName oncePerReport:(BOOL)once 842 { 843 if (![metric isKindOfClass:[NSNumber class]] || ![metricName isKindOfClass:[NSString class]]) { 844 secerror("SFAnalytics: Need a valid result and name to log result"); 845 return; 846 } 847 848 __weak __typeof(self) weakSelf = self; 849 dispatch_async(_queue, ^{ 850 os_transaction_t transaction = os_transaction_create("com.apple.security.sfanalytics.samplerGC"); 851 __strong __typeof(self) strongSelf = weakSelf; 852 if (strongSelf && !strongSelf->_disableLogging) { 853 if (once) { 854 [strongSelf.database removeAllSamplesForName:metricName]; 855 } 856 [strongSelf.database addSample:metric forName:metricName]; 857 } 858 (void)transaction; 859 transaction = nil; 860 }); 861 } 862 863 @end 864 865 #endif // __OBJC2__