BTAnalyticsService.m 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280
  1. #import "BTAnalyticsService.h"
  2. #import "BTAnalyticsMetadata.h"
  3. #import "BTAPIClient_Internal.h"
  4. #import "BTHTTP.h"
  5. #import "BTLogger_Internal.h"
  6. #if __has_include(<Braintree/BraintreeCore.h>)
  7. #import <Braintree/BTClientMetadata.h>
  8. #import <Braintree/BTClientToken.h>
  9. #import <Braintree/BTConfiguration.h>
  10. #import <Braintree/BTJSON.h>
  11. #else
  12. #import <BraintreeCore/BTClientMetadata.h>
  13. #import <BraintreeCore/BTClientToken.h>
  14. #import <BraintreeCore/BTConfiguration.h>
  15. #import <BraintreeCore/BTJSON.h>
  16. #endif
  17. #import <UIKit/UIKit.h>
  18. #pragma mark - BTAnalyticsEvent
  19. /// Encapsulates a single analytics event
  20. @interface BTAnalyticsEvent : NSObject
  21. @property (nonatomic, copy) NSString *kind;
  22. @property (nonatomic, assign) uint64_t timestamp;
  23. + (nonnull instancetype)event:(nonnull NSString *)eventKind withTimestamp:(uint64_t)timestamp;
  24. /// Event serialized to JSON
  25. - (nonnull NSDictionary *)json;
  26. @end
  27. @implementation BTAnalyticsEvent
  28. + (instancetype)event:(NSString *)eventKind withTimestamp:(uint64_t)timestamp {
  29. BTAnalyticsEvent *event = [[BTAnalyticsEvent alloc] init];
  30. event.kind = eventKind;
  31. event.timestamp = timestamp;
  32. return event;
  33. }
  34. - (NSString *)description {
  35. return [NSString stringWithFormat:@"%@ at %llu", self.kind, (uint64_t)self.timestamp];
  36. }
  37. - (NSDictionary *)json {
  38. return @{
  39. @"kind": self.kind,
  40. @"timestamp": @(self.timestamp)
  41. };
  42. }
  43. @end
  44. #pragma mark - BTAnalyticsSession
  45. /// Encapsulates analytics events for a given session
  46. @interface BTAnalyticsSession : NSObject
  47. @property (nonatomic, copy, nonnull) NSString *sessionID;
  48. @property (nonatomic, copy, nonnull) NSString *source;
  49. @property (nonatomic, copy, nonnull) NSString *integration;
  50. @property (nonatomic, strong, nonnull) NSMutableArray <BTAnalyticsEvent *> *events;
  51. /// Dictionary of analytics metadata from `BTAnalyticsMetadata`
  52. @property (nonatomic, strong, nonnull) NSDictionary *metadataParameters;
  53. + (nonnull instancetype)sessionWithID:(nonnull NSString *)sessionID
  54. source:(nonnull NSString *)source
  55. integration:(nonnull NSString *)integration;
  56. @end
  57. @implementation BTAnalyticsSession
  58. - (instancetype)init {
  59. if (self = [super init]) {
  60. _events = [NSMutableArray array];
  61. _metadataParameters = [BTAnalyticsMetadata metadata];
  62. }
  63. return self;
  64. }
  65. + (instancetype)sessionWithID:(NSString *)sessionID
  66. source:(NSString *)source
  67. integration:(NSString *)integration
  68. {
  69. if (!sessionID || !source || !integration) {
  70. return nil;
  71. }
  72. BTAnalyticsSession *session = [[BTAnalyticsSession alloc] init];
  73. session.sessionID = sessionID;
  74. session.source = source;
  75. session.integration = integration;
  76. return session;
  77. }
  78. @end
  79. #pragma mark - BTAnalyticsService
  80. @interface BTAnalyticsService ()
  81. /// Dictionary of analytics sessions, keyed by session ID. The analytics service requires that batched events
  82. /// are sent from only one session. In practice, BTAPIClient.metadata.sessionID should never change, so this
  83. /// is defensive.
  84. @property (nonatomic, strong) NSMutableDictionary <NSString *, BTAnalyticsSession *> *analyticsSessions;
  85. /// A serial dispatch queue that synchronizes access to `analyticsSessions`
  86. @property (nonatomic, strong) dispatch_queue_t sessionsQueue;
  87. @end
  88. @implementation BTAnalyticsService
  89. NSString * const BTAnalyticsServiceErrorDomain = @"com.braintreepayments.BTAnalyticsServiceErrorDomain";
  90. - (instancetype)initWithAPIClient:(BTAPIClient *)apiClient {
  91. if (self = [super init]) {
  92. _analyticsSessions = [NSMutableDictionary dictionary];
  93. _sessionsQueue = dispatch_queue_create("com.braintreepayments.BTAnalyticsService", DISPATCH_QUEUE_SERIAL);
  94. _apiClient = apiClient;
  95. _flushThreshold = 1;
  96. }
  97. return self;
  98. }
  99. - (void)dealloc {
  100. [[NSNotificationCenter defaultCenter] removeObserver:self];
  101. }
  102. #pragma mark - Public methods
  103. - (void)sendAnalyticsEvent:(NSString *)eventKind {
  104. dispatch_async(dispatch_get_main_queue(), ^{
  105. [self enqueueEvent:eventKind];
  106. [self checkFlushThreshold];
  107. });
  108. }
  109. - (void)sendAnalyticsEvent:(NSString *)eventKind completion:(__unused void(^)(NSError *error))completionBlock {
  110. dispatch_async(dispatch_get_main_queue(), ^{
  111. [self enqueueEvent:eventKind];
  112. [self flush:completionBlock];
  113. });
  114. }
  115. - (void)flush:(void (^)(NSError *))completionBlock {
  116. [self.apiClient fetchOrReturnRemoteConfiguration:^(BTConfiguration *configuration, NSError *error) {
  117. if (error) {
  118. [[BTLogger sharedLogger] warning:[NSString stringWithFormat:@"Failed to send analytics event. Remote configuration fetch failed. %@", error.localizedDescription]];
  119. if (completionBlock) completionBlock(error);
  120. return;
  121. }
  122. NSURL *analyticsURL = [configuration.json[@"analytics"][@"url"] asURL];
  123. if (!analyticsURL) {
  124. [[BTLogger sharedLogger] debug:@"Skipping sending analytics event - analytics is disabled in remote configuration"];
  125. NSError *error = [NSError errorWithDomain:BTAnalyticsServiceErrorDomain code:BTAnalyticsServiceErrorTypeMissingAnalyticsURL userInfo:@{ NSLocalizedDescriptionKey : @"Analytics is disabled in remote configuration" }];
  126. if (completionBlock) completionBlock(error);
  127. return;
  128. }
  129. if (!self.http) {
  130. if (self.apiClient.clientToken) {
  131. self.http = [[BTHTTP alloc] initWithBaseURL:analyticsURL authorizationFingerprint:self.apiClient.clientToken.authorizationFingerprint];
  132. } else if (self.apiClient.tokenizationKey) {
  133. self.http = [[BTHTTP alloc] initWithBaseURL:analyticsURL tokenizationKey:self.apiClient.tokenizationKey];
  134. }
  135. if (!self.http) {
  136. NSError *error = [NSError errorWithDomain:BTAnalyticsServiceErrorDomain code:BTAnalyticsServiceErrorTypeInvalidAPIClient userInfo:@{ NSLocalizedDescriptionKey : @"API client must have client token or tokenization key" }];
  137. [[BTLogger sharedLogger] warning:error.localizedDescription];
  138. if (completionBlock) completionBlock(error);
  139. return;
  140. }
  141. }
  142. // A special value passed in by unit tests to prevent BTHTTP from actually posting
  143. if ([self.http.baseURL isEqual:[NSURL URLWithString:@"test://do-not-send.url"]]) {
  144. if (completionBlock) completionBlock(nil);
  145. return;
  146. }
  147. dispatch_async(self.sessionsQueue, ^{
  148. if (self.analyticsSessions.count == 0) {
  149. if (completionBlock) completionBlock(nil);
  150. return;
  151. }
  152. BOOL willPostAnalyticsEvent = NO;
  153. for (NSString *sessionID in self.analyticsSessions.allKeys) {
  154. BTAnalyticsSession *session = self.analyticsSessions[sessionID];
  155. if (session.events.count == 0) {
  156. continue;
  157. }
  158. willPostAnalyticsEvent = YES;
  159. NSMutableDictionary *metadataParameters = [NSMutableDictionary dictionary];
  160. [metadataParameters addEntriesFromDictionary:session.metadataParameters];
  161. metadataParameters[@"sessionId"] = session.sessionID;
  162. metadataParameters[@"integrationType"] = session.integration;
  163. metadataParameters[@"source"] = session.source;
  164. NSMutableDictionary *postParameters = [NSMutableDictionary dictionary];
  165. if (session.events) {
  166. // Map array of BTAnalyticsEvent to JSON
  167. postParameters[@"analytics"] = [session.events valueForKey:@"json"];
  168. }
  169. postParameters[@"_meta"] = metadataParameters;
  170. if (self.apiClient.clientToken.authorizationFingerprint) {
  171. postParameters[@"authorization_fingerprint"] = self.apiClient.clientToken.authorizationFingerprint;
  172. }
  173. if (self.apiClient.tokenizationKey) {
  174. postParameters[@"tokenization_key"] = self.apiClient.tokenizationKey;
  175. }
  176. [session.events removeAllObjects];
  177. [self.http POST:@"/" parameters:postParameters completion:^(__unused BTJSON *body, __unused NSHTTPURLResponse *response, NSError *error) {
  178. if (error != nil) {
  179. [[BTLogger sharedLogger] warning:@"Failed to flush analytics events: %@", error.localizedDescription];
  180. }
  181. if (completionBlock) completionBlock(error);
  182. }];
  183. }
  184. if (!willPostAnalyticsEvent && completionBlock) {
  185. completionBlock(nil);
  186. }
  187. });
  188. }];
  189. }
  190. #pragma mark - Helpers
  191. - (void)enqueueEvent:(NSString *)eventKind {
  192. uint64_t timestampInMilliseconds = ([[NSDate date] timeIntervalSince1970] * 1000);
  193. BTAnalyticsEvent *event = [BTAnalyticsEvent event:eventKind withTimestamp:timestampInMilliseconds];
  194. BTAnalyticsSession *session = [BTAnalyticsSession sessionWithID:self.apiClient.metadata.sessionID
  195. source:self.apiClient.metadata.sourceString
  196. integration:self.apiClient.metadata.integrationString];
  197. if (!session) {
  198. [[BTLogger sharedLogger] warning:@"Missing analytics session metadata - will not send event %@", event.kind];
  199. return;
  200. }
  201. dispatch_async(self.sessionsQueue, ^{
  202. if (!self.analyticsSessions[session.sessionID]) {
  203. self.analyticsSessions[session.sessionID] = session;
  204. }
  205. [self.analyticsSessions[session.sessionID].events addObject:event];
  206. });
  207. }
  208. - (void)checkFlushThreshold {
  209. __block NSUInteger eventCount = 0;
  210. dispatch_sync(self.sessionsQueue, ^{
  211. for (BTAnalyticsSession *analyticsSession in self.analyticsSessions.allValues) {
  212. eventCount += analyticsSession.events.count;
  213. }
  214. });
  215. if (eventCount >= self.flushThreshold) {
  216. [self flush:nil];
  217. }
  218. }
  219. @end