]> git.saurik.com Git - apple/security.git/blob - Analytics/SFAnalytics.m
Security-59754.41.1.tar.gz
[apple/security.git] / Analytics / 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 mode_t permissions = 0775;
189 #else
190 mode_t permissions = 0700;
191 #endif // TARGET_OS_IPHONE
192 int ret = mkpath_np(path, permissions);
193 if (!(ret == 0 || ret == EEXIST)) {
194 secerror("could not create path: %s (%s)", path, strerror(ret));
195 }
196 chmod(path, permissions);
197 });
198 NSString *path = [NSString stringWithFormat:@"Analytics/%@.db", basename];
199 return [(__bridge_transfer NSURL*)SecCopyURLForFileInKeychainDirectory((__bridge CFStringRef)path) path];
200 }
201
202 + (NSString *)defaultProtectedAnalyticsDatabasePath:(NSString *)basename uuid:(NSUUID * __nullable)userUuid
203 {
204 // Create the top-level directory with full access
205 NSMutableString *directory = [NSMutableString stringWithString:@"sfanalytics"];
206 WithPathInProtectedDirectory((__bridge CFStringRef)directory, ^(const char *path) {
207 mode_t permissions = 0777;
208 int ret = mkpath_np(path, permissions);
209 if (!(ret == 0 || ret == EEXIST)) {
210 secerror("could not create path: %s (%s)", path, strerror(ret));
211 }
212 chmod(path, permissions);
213 });
214
215 // create per-user directory
216 if (userUuid) {
217 [directory appendString:@"/"];
218 [directory appendString:[userUuid UUIDString]];
219 WithPathInProtectedDirectory((__bridge CFStringRef)directory, ^(const char *path) {
220 #if TARGET_OS_IPHONE
221 mode_t permissions = 0775;
222 #else
223 mode_t permissions = 0700;
224 if (geteuid() == 0) {
225 // Root user directory needs to be read/write for group so that user supd can upload root data
226 permissions = 0775;
227 }
228 #endif // TARGET_OS_IPHONE
229 int ret = mkpath_np(path, permissions);
230 if (!(ret == 0 || ret == EEXIST)) {
231 secerror("could not create path: %s (%s)", path, strerror(ret));
232 }
233 chmod(path, permissions);
234 });
235 }
236 NSString *path = [NSString stringWithFormat:@"%@/%@.db", directory, basename];
237 return [(__bridge_transfer NSURL*)SecCopyURLForFileInProtectedDirectory((__bridge CFStringRef)path) path];
238 }
239
240 + (NSString *)defaultProtectedAnalyticsDatabasePath:(NSString *)basename
241 {
242 #if TARGET_OS_OSX
243 uid_t euid = geteuid();
244 uuid_t currentUserUuid;
245 int ret = mbr_uid_to_uuid(euid, currentUserUuid);
246 if (ret != 0) {
247 secerror("failed to get UUID for user(%d) - %d", euid, ret);
248 return [SFAnalytics defaultProtectedAnalyticsDatabasePath:basename uuid:nil];
249 }
250 NSUUID *userUuid = [[NSUUID alloc] initWithUUIDBytes:currentUserUuid];
251 return [SFAnalytics defaultProtectedAnalyticsDatabasePath:basename uuid:userUuid];
252 #else
253 return [SFAnalytics defaultProtectedAnalyticsDatabasePath:basename uuid:nil];
254 #endif // TARGET_OS_IPHONE
255 }
256
257 + (NSInteger)fuzzyDaysSinceDate:(NSDate*)date
258 {
259 // Sentinel: it didn't happen at all
260 if (!date) {
261 return -1;
262 }
263
264 // Sentinel: it happened but we don't know when because the date doesn't make sense
265 // Magic number represents January 1, 2017.
266 if ([date compare:[NSDate dateWithTimeIntervalSince1970:1483228800]] == NSOrderedAscending) {
267 return 1000;
268 }
269
270 NSInteger secondsPerDay = 60 * 60 * 24;
271
272 NSTimeInterval timeIntervalSinceDate = [[NSDate date] timeIntervalSinceDate:date];
273 if (timeIntervalSinceDate < secondsPerDay) {
274 return 0;
275 }
276 else if (timeIntervalSinceDate < (secondsPerDay * 7)) {
277 return 1;
278 }
279 else if (timeIntervalSinceDate < (secondsPerDay * 30)) {
280 return 7;
281 }
282 else if (timeIntervalSinceDate < (secondsPerDay * 365)) {
283 return 30;
284 }
285 else {
286 return 365;
287 }
288 }
289
290 // Instantiate lazily so unit tests can have clean databases each
291 - (SFAnalyticsSQLiteStore*)database
292 {
293 if (!_database) {
294 _database = [SFAnalyticsSQLiteStore storeWithPath:self.class.databasePath schema:SFAnalyticsTableSchema];
295 if (!_database) {
296 seccritical("Did not get a database! (Client %@)", NSStringFromClass([self class]));
297 }
298 }
299 return _database;
300 }
301
302 - (void)removeState
303 {
304 [_samplers removeAllObjects];
305 [_multisamplers removeAllObjects];
306
307 __weak __typeof(self) weakSelf = self;
308 dispatch_sync(_queue, ^{
309 __strong __typeof(self) strongSelf = weakSelf;
310 if (strongSelf) {
311 [strongSelf.database close];
312 strongSelf->_database = nil;
313 }
314 });
315 }
316
317 - (void)setDateProperty:(NSDate*)date forKey:(NSString*)key
318 {
319 __weak __typeof(self) weakSelf = self;
320 dispatch_sync(_queue, ^{
321 __strong __typeof(self) strongSelf = weakSelf;
322 if (strongSelf) {
323 [strongSelf.database setDateProperty:date forKey:key];
324 }
325 });
326 }
327
328 - (NSDate*)datePropertyForKey:(NSString*)key
329 {
330 __block NSDate* result = nil;
331 __weak __typeof(self) weakSelf = self;
332 dispatch_sync(_queue, ^{
333 __strong __typeof(self) strongSelf = weakSelf;
334 if (strongSelf) {
335 result = [strongSelf.database datePropertyForKey:key];
336 }
337 });
338 return result;
339 }
340
341
342 - (void)incrementIntegerPropertyForKey:(NSString*)key
343 {
344 __weak __typeof(self) weakSelf = self;
345 dispatch_sync(_queue, ^{
346 __strong __typeof(self) strongSelf = weakSelf;
347 if (strongSelf == nil) {
348 return;
349 }
350 NSInteger integer = [[strongSelf.database propertyForKey:key] integerValue];
351 [strongSelf.database setProperty:[NSString stringWithFormat:@"%ld", (long)integer + 1] forKey:key];
352 });
353 }
354
355 - (void)setNumberProperty:(NSNumber* _Nullable)number forKey:(NSString*)key
356 {
357 __weak __typeof(self) weakSelf = self;
358 dispatch_sync(_queue, ^{
359 __strong __typeof(self) strongSelf = weakSelf;
360 if (strongSelf) {
361 [strongSelf.database setProperty:[number stringValue] forKey:key];
362 }
363 });
364 }
365
366 - (NSNumber* _Nullable)numberPropertyForKey:(NSString*)key
367 {
368 __block NSNumber* result = nil;
369 __weak __typeof(self) weakSelf = self;
370 dispatch_sync(_queue, ^{
371 __strong __typeof(self) strongSelf = weakSelf;
372 if (strongSelf) {
373 NSString *property = [strongSelf.database propertyForKey:key];
374 if (property) {
375 result = [NSNumber numberWithInteger:[property integerValue]];
376 }
377 }
378 });
379 return result;
380 }
381
382 + (NSString*)hwModelID
383 {
384 static NSString *hwModel = nil;
385 static dispatch_once_t onceToken;
386 dispatch_once(&onceToken, ^{
387 #if TARGET_OS_SIMULATOR
388 // Asking for a real value in the simulator gives the results for the underlying mac. Not particularly useful.
389 hwModel = [NSString stringWithFormat:@"%s", getenv("SIMULATOR_MODEL_IDENTIFIER")];
390 #elif TARGET_OS_OSX
391 size_t size;
392 sysctlbyname("hw.model", NULL, &size, NULL, 0);
393 char *sysctlString = malloc(size);
394 sysctlbyname("hw.model", sysctlString, &size, NULL, 0);
395 hwModel = [[NSString alloc] initWithUTF8String:sysctlString];
396 free(sysctlString);
397 #else
398 struct utsname systemInfo;
399 uname(&systemInfo);
400
401 hwModel = [NSString stringWithCString:systemInfo.machine
402 encoding:NSUTF8StringEncoding];
403 #endif
404 });
405 return hwModel;
406 }
407
408 + (void)addOSVersionToEvent:(NSMutableDictionary*)eventDict {
409 static dispatch_once_t onceToken;
410 static NSString *build = NULL;
411 static NSString *product = NULL;
412 static NSString *modelID = nil;
413 static BOOL internal = NO;
414 dispatch_once(&onceToken, ^{
415 NSDictionary *version = CFBridgingRelease(_CFCopySystemVersionDictionary());
416 if (version == NULL)
417 return;
418 build = version[(__bridge NSString *)_kCFSystemVersionBuildVersionKey];
419 product = version[(__bridge NSString *)_kCFSystemVersionProductNameKey];
420 internal = os_variant_has_internal_diagnostics("com.apple.security");
421
422 modelID = [self hwModelID];
423 });
424 if (build) {
425 eventDict[SFAnalyticsEventBuild] = build;
426 }
427 if (product) {
428 eventDict[SFAnalyticsEventProduct] = product;
429 }
430 if (modelID) {
431 eventDict[SFAnalyticsEventModelID] = modelID;
432 }
433 if (internal) {
434 eventDict[SFAnalyticsEventInternal] = @YES;
435 }
436 }
437
438 - (instancetype)init
439 {
440 if (self = [super init]) {
441 _queue = dispatch_queue_create("SFAnalytics data access queue", DISPATCH_QUEUE_SERIAL_WITH_AUTORELEASE_POOL);
442 _samplers = [NSMutableDictionary<NSString*, SFAnalyticsSampler*> new];
443 _multisamplers = [NSMutableDictionary<NSString*, SFAnalyticsMultiSampler*> new];
444 [self database]; // for side effect of instantiating DB object. Used for testing.
445 }
446
447 return self;
448 }
449
450 - (NSDictionary *)coreAnalyticsKeyFilter:(NSDictionary<NSString *, id> *)info
451 {
452 NSMutableDictionary *filtered = [NSMutableDictionary dictionary];
453 [info enumerateKeysAndObjectsUsingBlock:^(NSString *_Nonnull key, id _Nonnull obj, BOOL * _Nonnull stop) {
454 filtered[[key stringByReplacingOccurrencesOfString:@"-" withString:@"_"]] = obj;
455 }];
456 return filtered;
457 }
458
459 // Daily CoreAnalytics metrics
460 // Call this once per say if you want to have the once per day sampler collect their data and submit it
461
462 - (void)dailyCoreAnalyticsMetrics:(NSString *)eventName
463 {
464 NSMutableDictionary<NSString*, NSNumber*> *dailyMetrics = [NSMutableDictionary dictionary];
465 __block NSDictionary<NSString*, SFAnalyticsMultiSampler*>* multisamplers;
466 __block NSDictionary<NSString*, SFAnalyticsSampler*>* samplers;
467
468 dispatch_sync(_queue, ^{
469 multisamplers = [self->_multisamplers copy];
470 samplers = [self->_samplers copy];
471 });
472
473 [multisamplers enumerateKeysAndObjectsUsingBlock:^(NSString * _Nonnull key, SFAnalyticsMultiSampler * _Nonnull obj, BOOL * _Nonnull stop) {
474 if (obj.oncePerReport == FALSE) {
475 return;
476 }
477 NSDictionary<NSString*, NSNumber*> *samples = [obj sampleNow];
478 if (samples == nil) {
479 return;
480 }
481 [dailyMetrics addEntriesFromDictionary:samples];
482 }];
483
484 [samplers enumerateKeysAndObjectsUsingBlock:^(NSString * _Nonnull key, SFAnalyticsSampler * _Nonnull obj, BOOL * _Nonnull stop) {
485 if (obj.oncePerReport == FALSE) {
486 return;
487 }
488 dailyMetrics[key] = [obj sampleNow];
489 }];
490
491 [SecCoreAnalytics sendEvent:eventName event:[self coreAnalyticsKeyFilter:dailyMetrics]];
492 }
493
494 // MARK: Event logging
495
496 - (void)logSuccessForEventNamed:(NSString*)eventName timestampBucket:(SFAnalyticsTimestampBucket)timestampBucket
497 {
498 [self logEventNamed:eventName class:SFAnalyticsEventClassSuccess attributes:nil timestampBucket:timestampBucket];
499 }
500
501 - (void)logSuccessForEventNamed:(NSString*)eventName
502 {
503 [self logSuccessForEventNamed:eventName timestampBucket:SFAnalyticsTimestampBucketSecond];
504 }
505
506 - (void)logHardFailureForEventNamed:(NSString*)eventName withAttributes:(NSDictionary*)attributes timestampBucket:(SFAnalyticsTimestampBucket)timestampBucket
507 {
508 [self logEventNamed:eventName class:SFAnalyticsEventClassHardFailure attributes:attributes timestampBucket:timestampBucket];
509 }
510
511 - (void)logHardFailureForEventNamed:(NSString*)eventName withAttributes:(NSDictionary*)attributes
512 {
513 [self logHardFailureForEventNamed:eventName withAttributes:attributes timestampBucket:SFAnalyticsTimestampBucketSecond];
514 }
515
516 - (void)logSoftFailureForEventNamed:(NSString*)eventName withAttributes:(NSDictionary*)attributes timestampBucket:(SFAnalyticsTimestampBucket)timestampBucket
517 {
518 [self logEventNamed:eventName class:SFAnalyticsEventClassSoftFailure attributes:attributes timestampBucket:timestampBucket];
519 }
520
521 - (void)logSoftFailureForEventNamed:(NSString*)eventName withAttributes:(NSDictionary*)attributes
522 {
523 [self logSoftFailureForEventNamed:eventName withAttributes:attributes timestampBucket:SFAnalyticsTimestampBucketSecond];
524 }
525
526 - (void)logResultForEvent:(NSString*)eventName hardFailure:(bool)hardFailure result:(NSError*)eventResultError timestampBucket:(SFAnalyticsTimestampBucket)timestampBucket
527 {
528 [self logResultForEvent:eventName hardFailure:hardFailure result:eventResultError withAttributes:nil timestampBucket:SFAnalyticsTimestampBucketSecond];
529 }
530
531 - (void)logResultForEvent:(NSString*)eventName hardFailure:(bool)hardFailure result:(NSError*)eventResultError
532 {
533 [self logResultForEvent:eventName hardFailure:hardFailure result:eventResultError timestampBucket:SFAnalyticsTimestampBucketSecond];
534 }
535
536 - (void)logResultForEvent:(NSString*)eventName hardFailure:(bool)hardFailure result:(NSError*)eventResultError withAttributes:(NSDictionary*)attributes timestampBucket:(SFAnalyticsTimestampBucket)timestampBucket
537 {
538 if(!eventResultError) {
539 [self logSuccessForEventNamed:eventName];
540 } else {
541 // Make an Attributes dictionary
542 NSMutableDictionary* eventAttributes = nil;
543 if (attributes) {
544 eventAttributes = [attributes mutableCopy];
545 } else {
546 eventAttributes = [NSMutableDictionary dictionary];
547 }
548
549 /* if we have underlying errors, capture the chain below the top-most error */
550 NSError *underlyingError = eventResultError.userInfo[NSUnderlyingErrorKey];
551 if ([underlyingError isKindOfClass:[NSError class]]) {
552 NSMutableString *chain = [NSMutableString string];
553 int count = 0;
554 do {
555 [chain appendFormat:@"%@-%ld:", underlyingError.domain, (long)underlyingError.code];
556 underlyingError = underlyingError.userInfo[NSUnderlyingErrorKey];
557 } while (count++ < 5 && [underlyingError isKindOfClass:[NSError class]]);
558
559 eventAttributes[SFAnalyticsAttributeErrorUnderlyingChain] = chain;
560 }
561
562 eventAttributes[SFAnalyticsAttributeErrorDomain] = eventResultError.domain;
563 eventAttributes[SFAnalyticsAttributeErrorCode] = @(eventResultError.code);
564
565 if(hardFailure) {
566 [self logHardFailureForEventNamed:eventName withAttributes:eventAttributes];
567 } else {
568 [self logSoftFailureForEventNamed:eventName withAttributes:eventAttributes];
569 }
570 }
571 }
572
573 - (void)logResultForEvent:(NSString*)eventName hardFailure:(bool)hardFailure result:(NSError*)eventResultError withAttributes:(NSDictionary*)attributes
574 {
575 [self logResultForEvent:eventName hardFailure:hardFailure result:eventResultError withAttributes:attributes timestampBucket:SFAnalyticsTimestampBucketSecond];
576 }
577
578 - (void)noteEventNamed:(NSString*)eventName timestampBucket:(SFAnalyticsTimestampBucket)timestampBucket
579 {
580 [self logEventNamed:eventName class:SFAnalyticsEventClassNote attributes:nil timestampBucket:timestampBucket];
581 }
582
583 - (void)noteEventNamed:(NSString*)eventName
584 {
585 [self noteEventNamed:eventName timestampBucket:SFAnalyticsTimestampBucketSecond];
586 }
587
588 - (void)logEventNamed:(NSString*)eventName class:(SFAnalyticsEventClass)class attributes:(NSDictionary*)attributes timestampBucket:(SFAnalyticsTimestampBucket)timestampBucket
589 {
590 if (!eventName) {
591 secerror("SFAnalytics: attempt to log an event with no name");
592 return;
593 }
594
595 __weak __typeof(self) weakSelf = self;
596 dispatch_sync(_queue, ^{
597 __strong __typeof(self) strongSelf = weakSelf;
598 if (!strongSelf || strongSelf->_disableLogging) {
599 return;
600 }
601
602 [strongSelf.database begin];
603
604 NSDictionary* eventDict = [self eventDictForEventName:eventName withAttributes:attributes eventClass:class timestampBucket:timestampBucket];
605
606 if (class == SFAnalyticsEventClassHardFailure) {
607 [strongSelf.database addEventDict:eventDict toTable:SFAnalyticsTableHardFailures timestampBucket:timestampBucket];
608 [strongSelf.database incrementHardFailureCountForEventType:eventName];
609 }
610 else if (class == SFAnalyticsEventClassSoftFailure) {
611 [strongSelf.database addEventDict:eventDict toTable:SFAnalyticsTableSoftFailures timestampBucket:timestampBucket];
612 [strongSelf.database incrementSoftFailureCountForEventType:eventName];
613 }
614 else if (class == SFAnalyticsEventClassNote) {
615 [strongSelf.database addEventDict:eventDict toTable:SFAnalyticsTableNotes timestampBucket:timestampBucket];
616 [strongSelf.database incrementSuccessCountForEventType:eventName];
617 }
618 else if (class == SFAnalyticsEventClassSuccess) {
619 [strongSelf.database incrementSuccessCountForEventType:eventName];
620 }
621
622 [strongSelf.database end];
623 });
624 }
625
626 - (void)logEventNamed:(NSString*)eventName class:(SFAnalyticsEventClass)class attributes:(NSDictionary*)attributes
627 {
628 [self logEventNamed:eventName class:class attributes:attributes timestampBucket:SFAnalyticsTimestampBucketSecond];
629 }
630
631 - (NSDictionary*) eventDictForEventName:(NSString*)eventName withAttributes:(NSDictionary*)attributes eventClass:(SFAnalyticsEventClass)eventClass timestampBucket:(NSTimeInterval)timestampBucket
632 {
633 NSMutableDictionary* eventDict = attributes ? attributes.mutableCopy : [NSMutableDictionary dictionary];
634 eventDict[SFAnalyticsEventType] = eventName;
635
636 NSTimeInterval timestamp = [[NSDate date] timeIntervalSince1970WithBucket:timestampBucket];
637
638 // our backend wants timestamps in milliseconds
639 eventDict[SFAnalyticsEventTime] = @(timestamp * 1000);
640 eventDict[SFAnalyticsEventClassKey] = @(eventClass);
641 [SFAnalytics addOSVersionToEvent:eventDict];
642
643 return eventDict;
644 }
645
646 // MARK: Sampling
647
648 - (SFAnalyticsSampler*)addMetricSamplerForName:(NSString *)samplerName withTimeInterval:(NSTimeInterval)timeInterval block:(NSNumber *(^)(void))block
649 {
650 if (!samplerName) {
651 secerror("SFAnalytics: cannot add sampler without name");
652 return nil;
653 }
654 if (timeInterval < 1.0f && timeInterval != SFAnalyticsSamplerIntervalOncePerReport) {
655 secerror("SFAnalytics: cannot add sampler with interval %f", timeInterval);
656 return nil;
657 }
658 if (!block) {
659 secerror("SFAnalytics: cannot add sampler without block");
660 return nil;
661 }
662
663 __block SFAnalyticsSampler* sampler = nil;
664
665 __weak __typeof(self) weakSelf = self;
666 dispatch_sync(_queue, ^{
667 __strong __typeof(self) strongSelf = weakSelf;
668 if (strongSelf->_samplers[samplerName]) {
669 secerror("SFAnalytics: sampler \"%@\" already exists", samplerName);
670 } else {
671 sampler = [[SFAnalyticsSampler alloc] initWithName:samplerName interval:timeInterval block:block clientClass:[self class]];
672 strongSelf->_samplers[samplerName] = sampler; // If sampler did not init because of bad data this 'removes' it from the dict, so a noop
673 }
674 });
675
676 return sampler;
677 }
678
679 - (SFAnalyticsMultiSampler*)AddMultiSamplerForName:(NSString *)samplerName withTimeInterval:(NSTimeInterval)timeInterval block:(NSDictionary<NSString *,NSNumber *> *(^)(void))block
680 {
681 if (!samplerName) {
682 secerror("SFAnalytics: cannot add sampler without name");
683 return nil;
684 }
685 if (timeInterval < 1.0f && timeInterval != SFAnalyticsSamplerIntervalOncePerReport) {
686 secerror("SFAnalytics: cannot add sampler with interval %f", timeInterval);
687 return nil;
688 }
689 if (!block) {
690 secerror("SFAnalytics: cannot add sampler without block");
691 return nil;
692 }
693
694 __block SFAnalyticsMultiSampler* sampler = nil;
695 __weak __typeof(self) weakSelf = self;
696 dispatch_sync(_queue, ^{
697 __strong __typeof(self) strongSelf = weakSelf;
698 if (strongSelf->_multisamplers[samplerName]) {
699 secerror("SFAnalytics: multisampler \"%@\" already exists", samplerName);
700 } else {
701 sampler = [[SFAnalyticsMultiSampler alloc] initWithName:samplerName interval:timeInterval block:block clientClass:[self class]];
702 strongSelf->_multisamplers[samplerName] = sampler;
703 }
704
705 });
706
707 return sampler;
708 }
709
710 - (SFAnalyticsSampler*)existingMetricSamplerForName:(NSString *)samplerName
711 {
712 __block SFAnalyticsSampler* sampler = nil;
713
714 __weak __typeof(self) weakSelf = self;
715 dispatch_sync(_queue, ^{
716 __strong __typeof(self) strongSelf = weakSelf;
717 if (strongSelf) {
718 sampler = strongSelf->_samplers[samplerName];
719 }
720 });
721 return sampler;
722 }
723
724 - (SFAnalyticsMultiSampler*)existingMultiSamplerForName:(NSString *)samplerName
725 {
726 __block SFAnalyticsMultiSampler* sampler = nil;
727
728 __weak __typeof(self) weakSelf = self;
729 dispatch_sync(_queue, ^{
730 __strong __typeof(self) strongSelf = weakSelf;
731 if (strongSelf) {
732 sampler = strongSelf->_multisamplers[samplerName];
733 }
734 });
735 return sampler;
736 }
737
738 - (void)removeMetricSamplerForName:(NSString *)samplerName
739 {
740 if (!samplerName) {
741 secerror("Attempt to remove sampler without specifying samplerName");
742 return;
743 }
744
745 __weak __typeof(self) weakSelf = self;
746 dispatch_async(_queue, ^{
747 os_transaction_t transaction = os_transaction_create("com.apple.security.sfanalytics.samplerGC");
748 __strong __typeof(self) strongSelf = weakSelf;
749 if (strongSelf) {
750 [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
751 [strongSelf->_samplers removeObjectForKey:samplerName];
752 }
753 (void)transaction;
754 transaction = nil;
755 });
756 }
757
758 - (void)removeMultiSamplerForName:(NSString *)samplerName
759 {
760 if (!samplerName) {
761 secerror("Attempt to remove multisampler without specifying samplerName");
762 return;
763 }
764
765 __weak __typeof(self) weakSelf = self;
766 dispatch_async(_queue, ^{
767 os_transaction_t transaction = os_transaction_create("com.apple.security.sfanalytics.samplerGC");
768 __strong __typeof(self) strongSelf = weakSelf;
769 if (strongSelf) {
770 [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
771 [strongSelf->_multisamplers removeObjectForKey:samplerName];
772 }
773 (void)transaction;
774 transaction = nil;
775 });
776 }
777
778 - (SFAnalyticsActivityTracker*)logSystemMetricsForActivityNamed:(NSString *)eventName withAction:(void (^)(void))action
779 {
780 if (![eventName isKindOfClass:[NSString class]]) {
781 secerror("Cannot log system metrics without name");
782 return nil;
783 }
784 SFAnalyticsActivityTracker* tracker = [[SFAnalyticsActivityTracker alloc] initWithName:eventName clientClass:[self class]];
785 if (action) {
786 [tracker performAction:action];
787 }
788 return tracker;
789 }
790
791 - (SFAnalyticsActivityTracker*)startLogSystemMetricsForActivityNamed:(NSString *)eventName
792 {
793 if (![eventName isKindOfClass:[NSString class]]) {
794 secerror("Cannot log system metrics without name");
795 return nil;
796 }
797 SFAnalyticsActivityTracker* tracker = [[SFAnalyticsActivityTracker alloc] initWithName:eventName clientClass:[self class]];
798 [tracker start];
799 return tracker;
800 }
801
802 - (void)logMetric:(NSNumber *)metric withName:(NSString *)metricName
803 {
804 [self logMetric:metric withName:metricName oncePerReport:NO];
805 }
806
807 - (void)logMetric:(NSNumber*)metric withName:(NSString*)metricName oncePerReport:(BOOL)once
808 {
809 if (![metric isKindOfClass:[NSNumber class]] || ![metricName isKindOfClass:[NSString class]]) {
810 secerror("SFAnalytics: Need a valid result and name to log result");
811 return;
812 }
813
814 __weak __typeof(self) weakSelf = self;
815 dispatch_async(_queue, ^{
816 os_transaction_t transaction = os_transaction_create("com.apple.security.sfanalytics.samplerGC");
817 __strong __typeof(self) strongSelf = weakSelf;
818 if (strongSelf && !strongSelf->_disableLogging) {
819 if (once) {
820 [strongSelf.database removeAllSamplesForName:metricName];
821 }
822 [strongSelf.database addSample:metric forName:metricName];
823 }
824 (void)transaction;
825 transaction = nil;
826 });
827 }
828
829 @end
830
831 #endif // __OBJC2__