PPOTAnalyticsTracker.m 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357
  1. //
  2. // PPOTAnalyticsTracker.m
  3. // PayPalOneTouch
  4. //
  5. // Copyright © 2014 PayPal, Inc. All rights reserved.
  6. //
  7. #import <UIKit/UIKit.h>
  8. #import "PPOTAnalyticsTracker.h"
  9. #import "PPOTCore_Internal.h"
  10. #if __has_include("PayPalUtils.h")
  11. #import "PPOTVersion.h"
  12. #import "PPOTDevice.h"
  13. #import "PPOTMacros.h"
  14. #import "PPOTSimpleKeychain.h"
  15. #import "PPOTString.h"
  16. #import "PPOTURLSession.h"
  17. #else
  18. #import <PayPalUtils/PPOTVersion.h>
  19. #import <PayPalUtils/PPOTDevice.h>
  20. #import <PayPalUtils/PPOTMacros.h>
  21. #import <PayPalUtils/PPOTSimpleKeychain.h>
  22. #import <PayPalUtils/PPOTString.h>
  23. #import <PayPalUtils/PPOTURLSession.h>
  24. #endif
  25. #import "PPFPTIData.h"
  26. #import "PPFPTITracker.h"
  27. #import "PPOTAnalyticsDefines.h"
  28. #if __has_include("BraintreeCore.h")
  29. #import "BTLogger_Internal.h"
  30. #else
  31. #import <BraintreeCore/BTLogger_Internal.h>
  32. #endif
  33. #define kTimeForSession (30 * 60) // How long should an Omniture session last.
  34. #define kKeychainIdentifierForUDID @"PayPal_OTC_Analytics_UDID"
  35. #define kFPTIVersKey @"vers"
  36. #define kFPTIPageKey @"page"
  37. #define kFPTILginKey @"lgin"
  38. #define kFPTIRstaKey @"rsta"
  39. #define kFPTIMosvKey @"mosv"
  40. #define kFPTIMdvsKey @"mdvs"
  41. #define kFPTIMapvKey @"mapv"
  42. #define kFPTIEccdKey @"eccd"
  43. #define kFPTIErpgKey @"erpg"
  44. #define kFPTIFltkKey @"fltk"
  45. #define kFPTITxntKey @"txnt"
  46. #define kFPTIApinKey @"apin"
  47. #define kFPTIBchnKey @"bchn"
  48. #define kFPTISrceKey @"srce"
  49. #define kFPTIBzsrKey @"bzsr"
  50. #define kFPTIPgrpKey @"pgrp"
  51. #define kFPTIVidKey @"vid"
  52. #define kFPTIDsidKey @"dsid"
  53. #define kFPTIClidKey @"clid"
  54. @interface PPOTAnalyticsTracker () <PPFPTINetworkAdapterDelegate>
  55. @property (nonatomic, copy, readwrite) NSString *deviceUDID;
  56. @property (nonatomic, copy, readwrite) NSDictionary *trackerParams;
  57. @property (nonatomic, assign, readwrite) BOOL sessionImpressionSent;
  58. @property (nonatomic, copy, readwrite) NSString *sessionID;
  59. @property (nonatomic, copy, readwrite) NSDate *lastImpressionDate;
  60. @property (nonatomic, strong, readwrite) NSMutableArray *apiEndpoints;
  61. @property (nonatomic, strong, readwrite) NSMutableArray *apiRoundtripTimes;
  62. @property (nonatomic, strong, readwrite) PPFPTITracker *fptiTracker;
  63. @property (nonatomic, strong, readwrite) PPOTURLSession *urlSession;
  64. @end
  65. @implementation PPOTAnalyticsTracker
  66. + (nonnull PPOTAnalyticsTracker *)sharedManager {
  67. static PPOTAnalyticsTracker* sharedManager = nil;
  68. static dispatch_once_t pred;
  69. dispatch_once(&pred, ^{
  70. sharedManager = [[PPOTAnalyticsTracker alloc] init];
  71. sharedManager.deviceUDID = [[NSString alloc] initWithData:[PPOTSimpleKeychain dataForKey:kKeychainIdentifierForUDID]
  72. encoding:NSUTF8StringEncoding];
  73. if ([sharedManager.deviceUDID length] == 0) {
  74. sharedManager.deviceUDID = [PPOTString generateUniquishIdentifier];
  75. [PPOTSimpleKeychain setData:[sharedManager.deviceUDID dataUsingEncoding:NSUTF8StringEncoding]
  76. forKey:kKeychainIdentifierForUDID];
  77. }
  78. // trackingVars are the standard params to add to every request
  79. NSMutableDictionary *trackingVars = [[NSMutableDictionary alloc] init];
  80. trackingVars[kFPTIMapvKey] = PayPalOTVersion(); // "App" Version number
  81. trackingVars[kFPTIRstaKey] = [[self class] deviceLocale]; // Locale (consumer app bases this on payerCountry)
  82. trackingVars[kFPTIMosvKey] = [NSString stringWithFormat:@"iOS %@", [UIDevice currentDevice].systemVersion]; // Mobile OS + version
  83. trackingVars[kFPTIMdvsKey] = [PPOTDevice deviceName]; // Mobile Device Name, i.e. iPhone 4S
  84. sharedManager.trackerParams = trackingVars;
  85. // Initialize FPTI:
  86. sharedManager.fptiTracker = [[PPFPTITracker alloc] initWithDeviceUDID:sharedManager.deviceUDID
  87. sessionID:sharedManager.sessionID
  88. networkAdapterDelegate:sharedManager];
  89. });
  90. return sharedManager;
  91. }
  92. - (nonnull instancetype)init {
  93. if (self = [super init]) {
  94. _apiRoundtripTimes = [NSMutableArray arrayWithCapacity:4];
  95. _apiEndpoints = [NSMutableArray arrayWithCapacity:4];
  96. _sessionImpressionSent = NO;
  97. }
  98. return self;
  99. }
  100. - (void)dealloc {
  101. [_urlSession finishTasksAndInvalidate];
  102. }
  103. #pragma mark - Smart getter for sessionID
  104. - (nonnull NSString *)sessionID {
  105. // For Omniture, the session should last 30 minutes
  106. if (_lastImpressionDate != nil && [[NSDate date] timeIntervalSinceDate:_lastImpressionDate] >= kTimeForSession) {
  107. _sessionID = nil;
  108. self.sessionImpressionSent = NO;
  109. }
  110. if (_sessionID == nil) {
  111. _sessionID = [PPOTAnalyticsTracker newOmnitureSessionID];
  112. }
  113. return _sessionID;
  114. }
  115. /**
  116. Generates a session ID
  117. @return a session ID
  118. */
  119. + (nonnull NSString *)newOmnitureSessionID {
  120. // The javascript is sed=Math&&Math.random?Math.floor(Math.random()*10000000000000):tm.getTime(),sess='s'+Math.floor(tm.getTime()/10800000)%10+sed
  121. // JavaScript Math.random gives a value between 0 and 1
  122. srandom((unsigned)[[NSDate date] timeIntervalSince1970]); // Seed the random number generator
  123. NSUInteger rnumber = (NSUInteger) (10000000000000 * ((float) random() / (float) RAND_MAX));
  124. // Javascript getTime is # of milliseconds from 1/1/1970
  125. return [NSString stringWithFormat:@"%lu", (unsigned long)(rnumber + (NSUInteger)floor((([NSDate timeIntervalSinceReferenceDate] + NSTimeIntervalSince1970) / (float) 10800)) % 10)];
  126. }
  127. #pragma mark -
  128. - (void)trackPage:(nonnull NSString *)pagename
  129. environment:(nonnull NSString *)environment
  130. clientID:(nullable NSString *)clientID
  131. error:(nullable NSError *)error
  132. hermesToken:(nullable NSString *)hermesToken {
  133. // Use PPAsserts to catch bad parameters in Debug version:
  134. PPAssert([pagename length], @"pagename must be non-empty");
  135. PPAssert(environment, @"environment can't be nil (can be empty string, though)");
  136. PPAssert(clientID, @"clientID can't be nil (can be empty string, though)");
  137. // Sanity-check parameters to prevent crashes in Release version:
  138. if (![pagename length]) {
  139. return;
  140. }
  141. if (!environment) {
  142. environment = @"";
  143. }
  144. if (!clientID) {
  145. clientID = @"";
  146. }
  147. NSMutableDictionary *params = [[NSMutableDictionary alloc] init];
  148. NSString *version = [kAnalyticsVersion stringByReplacingOccurrencesOfString:@"MODE" withString:environment];
  149. NSString *nameStr = [NSString stringWithFormat:@"%@:%@", pagename, version];
  150. if (error != nil) {
  151. nameStr = [nameStr stringByAppendingString:@"|error"];
  152. }
  153. params[kFPTIVersKey] = version;
  154. params[kFPTIPageKey] = nameStr;
  155. params[kFPTILginKey] = @"out";
  156. params[kFPTIPgrpKey] = (error == nil) ? pagename : [pagename stringByAppendingString:@"|error"];
  157. params[kFPTIBchnKey] = @"otc";
  158. params[kFPTISrceKey] = @"otc";
  159. params[kFPTIBzsrKey] = @"mobile";
  160. params[kFPTIVidKey] = self.sessionID;
  161. params[kFPTIDsidKey] = self.deviceUDID;
  162. params[kFPTIClidKey] = clientID;
  163. params[@"e"] = @"im";
  164. params[@"apid"] = [self appBundleInformation];
  165. if ([hermesToken length]) {
  166. params[kFPTIFltkKey] = hermesToken;
  167. }
  168. [self addTrackerParamsTo:params];
  169. [self addAPIEndpointParamsTo:params];
  170. [self addErrorParamsTo:params withError:error];
  171. // Send to FPTI. In this case, the FPTITracker prepares/formats the data which then is sent back to this instance's
  172. // PPFPTINetworkAdapterDelegate method.
  173. [self.fptiTracker submitEventWithParams:params];
  174. }
  175. #pragma mark - Helper methods for tracker data
  176. /**
  177. Adds tracker level parameters to the params request. Tracker level params are constant parameters (say a device name)
  178. which do not need to be re-calculated.
  179. @param params dictionary to add data to
  180. */
  181. - (void)addTrackerParamsTo:(nonnull NSMutableDictionary *)params {
  182. // If there is a standard set of properties/parameters to send on each call, add it now.
  183. if (self.trackerParams != nil && [self.trackerParams count]) {
  184. if (_sessionImpressionSent) {
  185. // Always send the rsta value
  186. params[@"rsta"] = self.trackerParams[@"rsta"];
  187. }
  188. else {
  189. [params addEntriesFromDictionary:self.trackerParams];
  190. }
  191. }
  192. }
  193. /**
  194. Return the bundle information for analytics
  195. @return bundle information as a string
  196. */
  197. - (nonnull NSString *)appBundleInformation {
  198. NSDictionary *infoDictionary = [[NSBundle mainBundle] infoDictionary];
  199. return [NSString stringWithFormat:@"%@|%@|%@|%@|%@|%@",
  200. infoDictionary[@"CFBundleExecutable"],
  201. infoDictionary[@"CFBundleIdentifier"],
  202. infoDictionary[@"CFBundleName"],
  203. infoDictionary[@"CFBundleShortVersionString"],
  204. infoDictionary[@"CFBundleVersion"],
  205. infoDictionary[@"CFBundleDisplayName"]];
  206. }
  207. /**
  208. Adds the API endpoint params
  209. @param params dictionary to add data to
  210. */
  211. - (void)addAPIEndpointParamsTo:(nonnull NSMutableDictionary *)params {
  212. if ([self.apiRoundtripTimes count]) {
  213. NSMutableString *timesString = [NSMutableString string];
  214. NSMutableString *endpointsString = [NSMutableString string];
  215. for (NSUInteger index = 0; index < [self.apiRoundtripTimes count]; index++) {
  216. if ([timesString length]) {
  217. [timesString appendString:@"|"];
  218. [endpointsString appendString:@"|"];
  219. }
  220. [timesString appendFormat:@"%ld", (long)[self.apiRoundtripTimes[index] integerValue]];
  221. [endpointsString appendString:self.apiEndpoints[index]];
  222. }
  223. params[kFPTITxntKey] = timesString;
  224. params[kFPTIApinKey] = endpointsString;
  225. [self.apiRoundtripTimes removeAllObjects];
  226. [self.apiEndpoints removeAllObjects];
  227. }
  228. }
  229. /**
  230. Adds the error param information (if there is an error
  231. @param params dictionary to add data to
  232. @param error the error
  233. */
  234. - (void)addErrorParamsTo:(nonnull NSMutableDictionary *)params withError:(nullable NSError *)error {
  235. if (error != nil) {
  236. if ([error.domain isEqualToString:NSURLErrorDomain] && error.code == NSURLErrorCancelled) {
  237. // if Canceled, don't report as an error
  238. }
  239. else {
  240. params[kFPTIEccdKey] = [NSString stringWithFormat:@"%ld", (long)error.code];
  241. params[kFPTIErpgKey] = ([error.localizedDescription length] > 0) ? error.localizedDescription : @"Unknown error";
  242. }
  243. }
  244. }
  245. #pragma mark - PPFPTINetworkAdapterDelegate
  246. - (void)sendRequestWithData:(nonnull __attribute__((unused)) PPFPTIData*)fptiData {
  247. NSDictionary *fptiDataDictionary = [fptiData dataAsDictionary];
  248. NSDictionary *params = fptiDataDictionary[@"events"][@"event_params"];
  249. NSString *nameStr = params[@"page"];
  250. NSArray *pageComponents = [nameStr componentsSeparatedByString:@":"];
  251. NSString *environment = pageComponents[[pageComponents count] - 2];
  252. if ([environment isEqualToString:@"mock"]) {
  253. return;
  254. }
  255. NSURL* url = [fptiData trackerURL];
  256. NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
  257. request.HTTPShouldUsePipelining = YES;
  258. [request setHTTPMethod:@"POST"];
  259. [request setValue:[fptiData userAgent] forHTTPHeaderField:@"User-Agent"];
  260. [request setValue:@"application/json" forHTTPHeaderField:@"Content-Type"];
  261. NSError *error;
  262. NSData *fptiJSONData = [NSJSONSerialization dataWithJSONObject:fptiDataDictionary options:0 error:&error];
  263. if (fptiJSONData == nil && error != NULL) {
  264. // ignore the error
  265. } else {
  266. [request setHTTPBody:fptiJSONData];
  267. [self.urlSession sendRequest:request completionBlock:nil];
  268. }
  269. }
  270. #pragma mark -
  271. + (NSString *)deviceLocale {
  272. // unlike NSLocaleIdentifier, this will always be either just language (@"en") or else language_COUNTRY (@"en_US")
  273. NSString *language = [[self class] deviceLanguage];
  274. NSString *country = [[self class] deviceCountryCode];
  275. if ([country length]) {
  276. return [NSString stringWithFormat:@"%@_%@", language, country];
  277. }
  278. else {
  279. return language;
  280. }
  281. }
  282. + (NSString *)deviceLanguage {
  283. return [[NSLocale currentLocale] objectForKey:NSLocaleLanguageCode];
  284. }
  285. + (NSString *)deviceCountryCode {
  286. //gives the country code from the device
  287. NSString *countryCode = [[NSLocale currentLocale] objectForKey:NSLocaleCountryCode];
  288. if (!countryCode) {
  289. // NSLocaleCountryCode can return nil if device's Region is set to English, Esperanto, etc.
  290. countryCode = @"";
  291. }
  292. return countryCode;
  293. }
  294. @end