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