| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357 |
- //
- // PPOTAnalyticsTracker.m
- // PayPalOneTouch
- //
- // Copyright © 2014 PayPal, Inc. All rights reserved.
- //
- #import <UIKit/UIKit.h>
- #import "PPOTAnalyticsTracker.h"
- #import "PPOTCore_Internal.h"
- #if __has_include("PayPalUtils.h")
- #import "PPOTVersion.h"
- #import "PPOTDevice.h"
- #import "PPOTMacros.h"
- #import "PPOTSimpleKeychain.h"
- #import "PPOTString.h"
- #import "PPOTURLSession.h"
- #else
- #import <PayPalUtils/PPOTVersion.h>
- #import <PayPalUtils/PPOTDevice.h>
- #import <PayPalUtils/PPOTMacros.h>
- #import <PayPalUtils/PPOTSimpleKeychain.h>
- #import <PayPalUtils/PPOTString.h>
- #import <PayPalUtils/PPOTURLSession.h>
- #endif
- #import "PPFPTIData.h"
- #import "PPFPTITracker.h"
- #import "PPOTAnalyticsDefines.h"
- #if __has_include("BraintreeCore.h")
- #import "BTLogger_Internal.h"
- #else
- #import <BraintreeCore/BTLogger_Internal.h>
- #endif
- #define kTimeForSession (30 * 60) // How long should an Omniture session last.
- #define kKeychainIdentifierForUDID @"PayPal_OTC_Analytics_UDID"
- #define kFPTIVersKey @"vers"
- #define kFPTIPageKey @"page"
- #define kFPTILginKey @"lgin"
- #define kFPTIRstaKey @"rsta"
- #define kFPTIMosvKey @"mosv"
- #define kFPTIMdvsKey @"mdvs"
- #define kFPTIMapvKey @"mapv"
- #define kFPTIEccdKey @"eccd"
- #define kFPTIErpgKey @"erpg"
- #define kFPTIFltkKey @"fltk"
- #define kFPTITxntKey @"txnt"
- #define kFPTIApinKey @"apin"
- #define kFPTIBchnKey @"bchn"
- #define kFPTISrceKey @"srce"
- #define kFPTIBzsrKey @"bzsr"
- #define kFPTIPgrpKey @"pgrp"
- #define kFPTIVidKey @"vid"
- #define kFPTIDsidKey @"dsid"
- #define kFPTIClidKey @"clid"
- @interface PPOTAnalyticsTracker () <PPFPTINetworkAdapterDelegate>
- @property (nonatomic, copy, readwrite) NSString *deviceUDID;
- @property (nonatomic, copy, readwrite) NSDictionary *trackerParams;
- @property (nonatomic, assign, readwrite) BOOL sessionImpressionSent;
- @property (nonatomic, copy, readwrite) NSString *sessionID;
- @property (nonatomic, copy, readwrite) NSDate *lastImpressionDate;
- @property (nonatomic, strong, readwrite) NSMutableArray *apiEndpoints;
- @property (nonatomic, strong, readwrite) NSMutableArray *apiRoundtripTimes;
- @property (nonatomic, strong, readwrite) PPFPTITracker *fptiTracker;
- @property (nonatomic, strong, readwrite) PPOTURLSession *urlSession;
- @end
- @implementation PPOTAnalyticsTracker
- + (nonnull PPOTAnalyticsTracker *)sharedManager {
- static PPOTAnalyticsTracker* sharedManager = nil;
- static dispatch_once_t pred;
- dispatch_once(&pred, ^{
- sharedManager = [[PPOTAnalyticsTracker alloc] init];
- sharedManager.deviceUDID = [[NSString alloc] initWithData:[PPOTSimpleKeychain dataForKey:kKeychainIdentifierForUDID]
- encoding:NSUTF8StringEncoding];
- if ([sharedManager.deviceUDID length] == 0) {
- sharedManager.deviceUDID = [PPOTString generateUniquishIdentifier];
- [PPOTSimpleKeychain setData:[sharedManager.deviceUDID dataUsingEncoding:NSUTF8StringEncoding]
- forKey:kKeychainIdentifierForUDID];
- }
- // trackingVars are the standard params to add to every request
- NSMutableDictionary *trackingVars = [[NSMutableDictionary alloc] init];
- trackingVars[kFPTIMapvKey] = PayPalOTVersion(); // "App" Version number
- trackingVars[kFPTIRstaKey] = [[self class] deviceLocale]; // Locale (consumer app bases this on payerCountry)
- trackingVars[kFPTIMosvKey] = [NSString stringWithFormat:@"iOS %@", [UIDevice currentDevice].systemVersion]; // Mobile OS + version
- trackingVars[kFPTIMdvsKey] = [PPOTDevice deviceName]; // Mobile Device Name, i.e. iPhone 4S
- sharedManager.trackerParams = trackingVars;
- // Initialize FPTI:
- sharedManager.fptiTracker = [[PPFPTITracker alloc] initWithDeviceUDID:sharedManager.deviceUDID
- sessionID:sharedManager.sessionID
- networkAdapterDelegate:sharedManager];
- });
- return sharedManager;
- }
- - (nonnull instancetype)init {
- if (self = [super init]) {
- _apiRoundtripTimes = [NSMutableArray arrayWithCapacity:4];
- _apiEndpoints = [NSMutableArray arrayWithCapacity:4];
- _sessionImpressionSent = NO;
- }
- return self;
- }
- - (void)dealloc {
- [_urlSession finishTasksAndInvalidate];
- }
- #pragma mark - Smart getter for sessionID
- - (nonnull NSString *)sessionID {
- // For Omniture, the session should last 30 minutes
- if (_lastImpressionDate != nil && [[NSDate date] timeIntervalSinceDate:_lastImpressionDate] >= kTimeForSession) {
- _sessionID = nil;
- self.sessionImpressionSent = NO;
- }
- if (_sessionID == nil) {
- _sessionID = [PPOTAnalyticsTracker newOmnitureSessionID];
- }
- return _sessionID;
- }
- /**
- Generates a session ID
- @return a session ID
- */
- + (nonnull NSString *)newOmnitureSessionID {
- // The javascript is sed=Math&&Math.random?Math.floor(Math.random()*10000000000000):tm.getTime(),sess='s'+Math.floor(tm.getTime()/10800000)%10+sed
- // JavaScript Math.random gives a value between 0 and 1
- srandom((unsigned)[[NSDate date] timeIntervalSince1970]); // Seed the random number generator
- NSUInteger rnumber = (NSUInteger) (10000000000000 * ((float) random() / (float) RAND_MAX));
- // Javascript getTime is # of milliseconds from 1/1/1970
- return [NSString stringWithFormat:@"%lu", (unsigned long)(rnumber + (NSUInteger)floor((([NSDate timeIntervalSinceReferenceDate] + NSTimeIntervalSince1970) / (float) 10800)) % 10)];
- }
- #pragma mark -
- - (void)trackPage:(nonnull NSString *)pagename
- environment:(nonnull NSString *)environment
- clientID:(nullable NSString *)clientID
- error:(nullable NSError *)error
- hermesToken:(nullable NSString *)hermesToken {
- // Use PPAsserts to catch bad parameters in Debug version:
- PPAssert([pagename length], @"pagename must be non-empty");
- PPAssert(environment, @"environment can't be nil (can be empty string, though)");
- PPAssert(clientID, @"clientID can't be nil (can be empty string, though)");
- // Sanity-check parameters to prevent crashes in Release version:
- if (![pagename length]) {
- return;
- }
- if (!environment) {
- environment = @"";
- }
- if (!clientID) {
- clientID = @"";
- }
- NSMutableDictionary *params = [[NSMutableDictionary alloc] init];
- NSString *version = [kAnalyticsVersion stringByReplacingOccurrencesOfString:@"MODE" withString:environment];
- NSString *nameStr = [NSString stringWithFormat:@"%@:%@", pagename, version];
- if (error != nil) {
- nameStr = [nameStr stringByAppendingString:@"|error"];
- }
- params[kFPTIVersKey] = version;
- params[kFPTIPageKey] = nameStr;
- params[kFPTILginKey] = @"out";
- params[kFPTIPgrpKey] = (error == nil) ? pagename : [pagename stringByAppendingString:@"|error"];
- params[kFPTIBchnKey] = @"otc";
- params[kFPTISrceKey] = @"otc";
- params[kFPTIBzsrKey] = @"mobile";
- params[kFPTIVidKey] = self.sessionID;
- params[kFPTIDsidKey] = self.deviceUDID;
- params[kFPTIClidKey] = clientID;
- params[@"e"] = @"im";
- params[@"apid"] = [self appBundleInformation];
- if ([hermesToken length]) {
- params[kFPTIFltkKey] = hermesToken;
- }
- [self addTrackerParamsTo:params];
- [self addAPIEndpointParamsTo:params];
- [self addErrorParamsTo:params withError:error];
- // Send to FPTI. In this case, the FPTITracker prepares/formats the data which then is sent back to this instance's
- // PPFPTINetworkAdapterDelegate method.
- [self.fptiTracker submitEventWithParams:params];
- }
- #pragma mark - Helper methods for tracker data
- /**
- Adds tracker level parameters to the params request. Tracker level params are constant parameters (say a device name)
- which do not need to be re-calculated.
- @param params dictionary to add data to
- */
- - (void)addTrackerParamsTo:(nonnull NSMutableDictionary *)params {
- // If there is a standard set of properties/parameters to send on each call, add it now.
- if (self.trackerParams != nil && [self.trackerParams count]) {
- if (_sessionImpressionSent) {
- // Always send the rsta value
- params[@"rsta"] = self.trackerParams[@"rsta"];
- }
- else {
- [params addEntriesFromDictionary:self.trackerParams];
- }
- }
- }
- /**
- Return the bundle information for analytics
- @return bundle information as a string
- */
- - (nonnull NSString *)appBundleInformation {
- NSDictionary *infoDictionary = [[NSBundle mainBundle] infoDictionary];
- return [NSString stringWithFormat:@"%@|%@|%@|%@|%@|%@",
- infoDictionary[@"CFBundleExecutable"],
- infoDictionary[@"CFBundleIdentifier"],
- infoDictionary[@"CFBundleName"],
- infoDictionary[@"CFBundleShortVersionString"],
- infoDictionary[@"CFBundleVersion"],
- infoDictionary[@"CFBundleDisplayName"]];
- }
- /**
- Adds the API endpoint params
- @param params dictionary to add data to
- */
- - (void)addAPIEndpointParamsTo:(nonnull NSMutableDictionary *)params {
- if ([self.apiRoundtripTimes count]) {
- NSMutableString *timesString = [NSMutableString string];
- NSMutableString *endpointsString = [NSMutableString string];
- for (NSUInteger index = 0; index < [self.apiRoundtripTimes count]; index++) {
- if ([timesString length]) {
- [timesString appendString:@"|"];
- [endpointsString appendString:@"|"];
- }
- [timesString appendFormat:@"%ld", (long)[self.apiRoundtripTimes[index] integerValue]];
- [endpointsString appendString:self.apiEndpoints[index]];
- }
- params[kFPTITxntKey] = timesString;
- params[kFPTIApinKey] = endpointsString;
- [self.apiRoundtripTimes removeAllObjects];
- [self.apiEndpoints removeAllObjects];
- }
- }
- /**
- Adds the error param information (if there is an error
- @param params dictionary to add data to
- @param error the error
- */
- - (void)addErrorParamsTo:(nonnull NSMutableDictionary *)params withError:(nullable NSError *)error {
- if (error != nil) {
- if ([error.domain isEqualToString:NSURLErrorDomain] && error.code == NSURLErrorCancelled) {
- // if Canceled, don't report as an error
- }
- else {
- params[kFPTIEccdKey] = [NSString stringWithFormat:@"%ld", (long)error.code];
- params[kFPTIErpgKey] = ([error.localizedDescription length] > 0) ? error.localizedDescription : @"Unknown error";
- }
- }
- }
- #pragma mark - PPFPTINetworkAdapterDelegate
- - (void)sendRequestWithData:(nonnull __attribute__((unused)) PPFPTIData*)fptiData {
- NSDictionary *fptiDataDictionary = [fptiData dataAsDictionary];
- NSDictionary *params = fptiDataDictionary[@"events"][@"event_params"];
- NSString *nameStr = params[@"page"];
- NSArray *pageComponents = [nameStr componentsSeparatedByString:@":"];
- NSString *environment = pageComponents[[pageComponents count] - 2];
- if ([environment isEqualToString:@"mock"]) {
- return;
- }
- NSURL* url = [fptiData trackerURL];
- NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
- request.HTTPShouldUsePipelining = YES;
- [request setHTTPMethod:@"POST"];
- [request setValue:[fptiData userAgent] forHTTPHeaderField:@"User-Agent"];
- [request setValue:@"application/json" forHTTPHeaderField:@"Content-Type"];
- NSError *error;
- NSData *fptiJSONData = [NSJSONSerialization dataWithJSONObject:fptiDataDictionary options:0 error:&error];
- if (fptiJSONData == nil && error != NULL) {
- // ignore the error
- } else {
- [request setHTTPBody:fptiJSONData];
- [self.urlSession sendRequest:request completionBlock:nil];
- }
- }
- #pragma mark -
- + (NSString *)deviceLocale {
- // unlike NSLocaleIdentifier, this will always be either just language (@"en") or else language_COUNTRY (@"en_US")
- NSString *language = [[self class] deviceLanguage];
- NSString *country = [[self class] deviceCountryCode];
- if ([country length]) {
- return [NSString stringWithFormat:@"%@_%@", language, country];
- }
- else {
- return language;
- }
- }
- + (NSString *)deviceLanguage {
- return [[NSLocale currentLocale] objectForKey:NSLocaleLanguageCode];
- }
- + (NSString *)deviceCountryCode {
- //gives the country code from the device
- NSString *countryCode = [[NSLocale currentLocale] objectForKey:NSLocaleCountryCode];
- if (!countryCode) {
- // NSLocaleCountryCode can return nil if device's Region is set to English, Esperanto, etc.
- countryCode = @"";
- }
- return countryCode;
- }
- @end
|