/ Analytics / SFAnalytics.m
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__