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