2
0

Tag screens in Localytics + email fix + psc fix

[FIXED]     Sending email with no recipient caused a crash.
[FIXED]     Duplicate persistence coordinator.
[UPDATED]   Crashlytics and Localytics SDKs.
[UPDATED]   Don't continue the Localytics session when device locked.
[UPDATED]   Put Localytics communication on HTTPS for confidentiality.
[ADDED]     Tagging screens in Localytics.
This commit is contained in:
Maarten Billemont 2012-09-12 15:58:54 +02:00
parent 6f82cf7f15
commit bce6b96417
22 changed files with 2807 additions and 2509 deletions

View File

@ -148,6 +148,26 @@ OBJC_EXTERN void CLSNSLog(NSString *format, ...);
@end @end
/**
* The CLSCrashReport protocol exposes methods that you can call on crash report objects passed
* to delegate methods. If you want these values or the entire object to stay in memory retain
* them or copy them.
**/
@protocol CLSCrashReport <NSObject>
@optional
/**
* Returns the session identifier for the crash report.
**/
- (NSString *)identifier;
/**
* Returns the custom key value data for the crash report.
**/
- (NSDictionary *)customKeys;
@end
/** /**
* *
* The CrashlyticsDelegate protocol provides a mechanism for your application to take * The CrashlyticsDelegate protocol provides a mechanism for your application to take
@ -169,4 +189,15 @@ OBJC_EXTERN void CLSNSLog(NSString *format, ...);
**/ **/
- (void)crashlyticsDidDetectCrashDuringPreviousExecution:(Crashlytics *)crashlytics; - (void)crashlyticsDidDetectCrashDuringPreviousExecution:(Crashlytics *)crashlytics;
/**
*
* Just like crashlyticsDidDetectCrashDuringPreviousExecution this delegate method is
* called once a Crashlytics instance has determined that the last execution of the
* application ended in a crash. A CLSCrashReport is passed back that contains data about
* the last crash report that was generated. See the CLSCrashReport protocol for method details.
* This method is called after crashlyticsDidDetectCrashDuringPreviousExecution.
*
**/
- (void)crashlytics:(Crashlytics *)crashlytics didDetectCrashDuringPreviousExecution:(id <CLSCrashReport>)crash;
@end @end

Binary file not shown.

2
External/Pearl vendored

@ -1 +1 @@
Subproject commit 4ceb992dc59e9d085b241239b63df4754da90de7 Subproject commit b8cf787af604299c5363846c5b2fcbd9e3083253

View File

@ -1,10 +1,12 @@
// //
// LocalyticsDatabase.h // LocalyticsDatabase.h
// LocalyticsDemo // Copyright (C) 2012 Char Software Inc., DBA Localytics
// //
// Created by jkaufman on 5/26/11. // This code is provided under the Localytics Modified BSD License.
// Copyright 2011 Localytics. All rights reserved. // A copy of this license has been distributed in a file called LICENSE
// with this source code.
// //
// Please visit www.localytics.com for more information.
#import <Foundation/Foundation.h> #import <Foundation/Foundation.h>
#import <sqlite3.h> #import <sqlite3.h>
@ -31,6 +33,8 @@
- (BOOL)addEventWithBlobString:(NSString *)blob; - (BOOL)addEventWithBlobString:(NSString *)blob;
- (BOOL)addCloseEventWithBlobString:(NSString *)blob; - (BOOL)addCloseEventWithBlobString:(NSString *)blob;
- (BOOL)queueCloseEventWithBlobString:(NSString *)blob;
- (NSString *)dequeueCloseEventBlobString;
- (BOOL)addFlowEventWithBlobString:(NSString *)blob; - (BOOL)addFlowEventWithBlobString:(NSString *)blob;
- (BOOL)removeLastCloseAndFlowEvents; - (BOOL)removeLastCloseAndFlowEvents;
@ -54,4 +58,13 @@
- (NSString *)customDimension:(int)dimension; - (NSString *)customDimension:(int)dimension;
- (BOOL)setCustomDimension:(int)dimension value:(NSString *)value; - (BOOL)setCustomDimension:(int)dimension value:(NSString *)value;
- (NSString *)customerId;
- (BOOL)setCustomerId:(NSString *)newCustomerId;
- (NSInteger)safeIntegerValueFromDictionary:(NSDictionary *)dict forKey:(NSString *)key;
- (NSString *)safeStringValueFromDictionary:(NSDictionary *)dict forKey:(NSString *)key;
- (NSDictionary *)safeDictionaryFromDictionary:(NSDictionary *)dict forKey:(NSString *)key;
- (NSArray *)safeListFromDictionary:(NSDictionary *)dict forKey:(NSString *)key;
@end @end

View File

@ -1,10 +1,12 @@
// //
// LocalyticsDatabase.m // LocalyticsDatabase.m
// LocalyticsDemo // Copyright (C) 2012 Char Software Inc., DBA Localytics
// //
// Created by jkaufman on 5/26/11. // This code is provided under the Localytics Modified BSD License.
// Copyright 2011 Localytics. All rights reserved. // A copy of this license has been distributed in a file called LICENSE
// with this source code.
// //
// Please visit www.localytics.com for more information.
#import "LocalyticsDatabase.h" #import "LocalyticsDatabase.h"
@ -17,6 +19,10 @@
- (void)createSchema; - (void)createSchema;
- (void)upgradeToSchemaV2; - (void)upgradeToSchemaV2;
- (void)upgradeToSchemaV3; - (void)upgradeToSchemaV3;
- (void)upgradeToSchemaV4;
- (void)upgradeToSchemaV5;
- (void)upgradeToSchemaV6;
- (void)upgradeToSchemaV7;
- (void)moveDbToCaches; - (void)moveDbToCaches;
- (NSString *)randomUUID; - (NSString *)randomUUID;
@end @end
@ -68,6 +74,12 @@ static LocalyticsDatabase *_sharedLocalyticsDatabase = nil;
code = sqlite3_open([dbPath UTF8String], &_databaseConnection); code = sqlite3_open([dbPath UTF8String], &_databaseConnection);
} }
// Enable foreign key constraints.
if (code == SQLITE_OK) {
const char *sql = [@"PRAGMA foreign_keys = ON;" cStringUsingEncoding:NSUTF8StringEncoding];
code = sqlite3_exec(_databaseConnection, sql, NULL, NULL, NULL);
}
// Check db connection, creating schema if necessary. // Check db connection, creating schema if necessary.
if (code == SQLITE_OK) { if (code == SQLITE_OK) {
sqlite3_busy_timeout(_databaseConnection, BUSY_TIMEOUT); // Defaults to 0, otherwise. sqlite3_busy_timeout(_databaseConnection, BUSY_TIMEOUT); // Defaults to 0, otherwise.
@ -83,6 +95,18 @@ static LocalyticsDatabase *_sharedLocalyticsDatabase = nil;
if ([self schemaVersion] < 3) { if ([self schemaVersion] < 3) {
[self upgradeToSchemaV3]; [self upgradeToSchemaV3];
} }
if ([self schemaVersion] < 4) {
[self upgradeToSchemaV4];
}
if ([self schemaVersion] < 5) {
[self upgradeToSchemaV5];
}
if ([self schemaVersion] < 6) {
[self upgradeToSchemaV6];
}
if ([self schemaVersion] < 7) {
[self upgradeToSchemaV7];
}
} }
return self; return self;
@ -286,9 +310,153 @@ static LocalyticsDatabase *_sharedLocalyticsDatabase = nil;
// V3 adds a field for the last app key and patches a V2 migration issue. // V3 adds a field for the last app key and patches a V2 migration issue.
- (void)upgradeToSchemaV3 { - (void)upgradeToSchemaV3 {
sqlite3_exec(_databaseConnection, int code = sqlite3_exec(_databaseConnection, "BEGIN", NULL, NULL, NULL);
"ALTER TABLE localytics_info ADD app_key CHAR(64)",
NULL, NULL, NULL); if (code == SQLITE_OK) {
sqlite3_exec(_databaseConnection,
"ALTER TABLE localytics_info ADD app_key CHAR(64)",
NULL, NULL, NULL);
}
if (code == SQLITE_OK) {
code = sqlite3_exec(_databaseConnection,
"UPDATE localytics_info set schema_version = 3",
NULL, NULL, NULL);
}
// Commit transaction.
if (code == SQLITE_OK || code == SQLITE_DONE) {
sqlite3_exec(_databaseConnection, "COMMIT", NULL, NULL, NULL);
} else {
sqlite3_exec(_databaseConnection, "ROLLBACK", NULL, NULL, NULL);
}
}
// V4 adds a field for the customer id.
- (void)upgradeToSchemaV4 {
int code = sqlite3_exec(_databaseConnection, "BEGIN", NULL, NULL, NULL);
if (code == SQLITE_OK) {
sqlite3_exec(_databaseConnection,
"ALTER TABLE localytics_info ADD customer_id CHAR(64)",
NULL, NULL, NULL);
}
if (code == SQLITE_OK) {
code = sqlite3_exec(_databaseConnection,
"UPDATE localytics_info set schema_version = 4",
NULL, NULL, NULL);
}
// Commit transaction.
if (code == SQLITE_OK || code == SQLITE_DONE) {
sqlite3_exec(_databaseConnection, "COMMIT", NULL, NULL, NULL);
} else {
sqlite3_exec(_databaseConnection, "ROLLBACK", NULL, NULL, NULL);
}
}
// V5 adds AMP related tables.
- (void)upgradeToSchemaV5 {
int code = sqlite3_exec(_databaseConnection, "BEGIN", NULL, NULL, NULL);
//The AMP DB table was initially created here. in Version 7 it will be dropped and re-added with the correct data types.
//therefore the code that creates it is no longer going to be called here.
//we still want to change the schema version
if (code == SQLITE_OK) {
code = sqlite3_exec(_databaseConnection,
"UPDATE localytics_info set schema_version = 5",
NULL, NULL, NULL);
}
// Commit transaction.
if (code == SQLITE_OK || code == SQLITE_DONE) {
sqlite3_exec(_databaseConnection, "COMMIT", NULL, NULL, NULL);
} else {
sqlite3_exec(_databaseConnection, "ROLLBACK", NULL, NULL, NULL);
}
}
// V6 adds a field for the queued close event blob string.
- (void)upgradeToSchemaV6 {
int code = sqlite3_exec(_databaseConnection, "BEGIN", NULL, NULL, NULL);
if (code == SQLITE_OK) {
sqlite3_exec(_databaseConnection,
"ALTER TABLE localytics_info ADD queued_close_event_blob TEXT",
NULL, NULL, NULL);
}
if (code == SQLITE_OK) {
code = sqlite3_exec(_databaseConnection,
"UPDATE localytics_info set schema_version = 6",
NULL, NULL, NULL);
}
// Commit transaction.
if (code == SQLITE_OK || code == SQLITE_DONE) {
sqlite3_exec(_databaseConnection, "COMMIT", NULL, NULL, NULL);
} else {
sqlite3_exec(_databaseConnection, "ROLLBACK", NULL, NULL, NULL);
}
}
-(void)upgradeToSchemaV7 {
int code = sqlite3_exec(_databaseConnection, "BEGIN", NULL, NULL, NULL);
if (code == SQLITE_OK) {
code = sqlite3_exec(_databaseConnection, "DROP TABLE IF EXISTS localytics_amp_rule", NULL, NULL, NULL);
}
if (code == SQLITE_OK) {
code = sqlite3_exec(_databaseConnection,
"CREATE TABLE IF NOT EXISTS localytics_amp_rule ("
"rule_id INTEGER PRIMARY KEY AUTOINCREMENT, "
"rule_name TEXT UNIQUE, "
"expiration INTEGER, "
"phone_location TEXT, "
"phone_size_width INTEGER, "
"phone_size_height INTEGER, "
"tablet_location TEXT, "
"tablet_size_width INTEGER, "
"tablet_size_height INTEGER, "
"display_seconds INTEGER, "
"display_session INTEGER, "
"version INTEGER, "
"did_display INTEGER, "
"times_to_display INTEGER, "
"internet_required INTEGER, "
"ab_test TEXT"
")",
NULL, NULL, NULL);
}
if (code == SQLITE_OK) {
code = sqlite3_exec(_databaseConnection,
"CREATE TABLE IF NOT EXISTS localytics_amp_ruleevent ("
"rule_id INTEGER, "
"event_name TEXT, "
"FOREIGN KEY(rule_id) REFERENCES localytics_amp_rule(rule_id) ON DELETE CASCADE "
")",
NULL, NULL, NULL);
}
if (code == SQLITE_OK) {
code = sqlite3_exec(_databaseConnection,
"UPDATE localytics_info set schema_version = 7",
NULL, NULL, NULL);
}
// Commit transaction.
if (code == SQLITE_OK || code == SQLITE_DONE) {
sqlite3_exec(_databaseConnection, "COMMIT", NULL, NULL, NULL);
} else {
sqlite3_exec(_databaseConnection, "ROLLBACK", NULL, NULL, NULL);
}
} }
- (NSUInteger)databaseSize { - (NSUInteger)databaseSize {
@ -333,7 +501,7 @@ static LocalyticsDatabase *_sharedLocalyticsDatabase = nil;
sqlite3_prepare_v2(_databaseConnection, "SELECT last_session_start FROM localytics_info", -1, &selectLastSessionStart, NULL); sqlite3_prepare_v2(_databaseConnection, "SELECT last_session_start FROM localytics_info", -1, &selectLastSessionStart, NULL);
int code = sqlite3_step(selectLastSessionStart); int code = sqlite3_step(selectLastSessionStart);
if (code == SQLITE_ROW) { if (code == SQLITE_ROW) {
lastSessionStart = sqlite3_column_double(selectLastSessionStart, 0) == 1; lastSessionStart = sqlite3_column_double(selectLastSessionStart, 0);
} }
sqlite3_finalize(selectLastSessionStart); sqlite3_finalize(selectLastSessionStart);
@ -517,6 +685,46 @@ static LocalyticsDatabase *_sharedLocalyticsDatabase = nil;
return success; return success;
} }
- (BOOL)queueCloseEventWithBlobString:(NSString *)blob {
NSString *t = @"queue_close_event";
BOOL success = [self beginTransaction:t];
// Queue close event.
if (success) {
sqlite3_stmt *queueCloseEvent;
sqlite3_prepare_v2(_databaseConnection, "UPDATE localytics_info SET queued_close_event_blob = ?", -1, &queueCloseEvent, NULL);
sqlite3_bind_text(queueCloseEvent, 1, [blob UTF8String], -1, SQLITE_TRANSIENT);
int code = sqlite3_step(queueCloseEvent);
sqlite3_finalize(queueCloseEvent);
success = code == SQLITE_DONE;
}
if (success) {
[self releaseTransaction:t];
} else {
[self rollbackTransaction:t];
}
return success;
}
- (NSString *)dequeueCloseEventBlobString {
NSString *value = nil;
NSString *query = @"SELECT queued_close_event_blob FROM localytics_info";
sqlite3_stmt *selectStmt;
sqlite3_prepare_v2(_databaseConnection, [query UTF8String], -1, &selectStmt, NULL);
int code = sqlite3_step(selectStmt);
if (code == SQLITE_ROW && sqlite3_column_text(selectStmt, 0)) {
value = [NSString stringWithUTF8String:(char *)sqlite3_column_text(selectStmt, 0)];
}
sqlite3_finalize(selectStmt);
// Clear the queued close event blob.
[self queueCloseEventWithBlobString:nil];
return value;
}
- (BOOL)addFlowEventWithBlobString:(NSString *)blob { - (BOOL)addFlowEventWithBlobString:(NSString *)blob {
NSString *t = @"add_flow_event"; NSString *t = @"add_flow_event";
BOOL success = [self beginTransaction:t]; BOOL success = [self beginTransaction:t];
@ -669,13 +877,23 @@ static LocalyticsDatabase *_sharedLocalyticsDatabase = nil;
code = sqlite3_exec(_databaseConnection, "DELETE FROM upload_headers", NULL, NULL, NULL); code = sqlite3_exec(_databaseConnection, "DELETE FROM upload_headers", NULL, NULL, NULL);
} }
if (code == SQLITE_OK) { if (code == SQLITE_OK) {
code = sqlite3_exec(_databaseConnection, "DELETE FROM localytics_amp_rule", NULL, NULL, NULL);
}
if (code == SQLITE_OK) {
code = sqlite3_exec(_databaseConnection, "DELETE FROM localytics_amp_ruleevent", NULL, NULL, NULL);
}
if (code == SQLITE_OK) {
code = sqlite3_exec(_databaseConnection,"UPDATE localytics_info SET last_session_number = 0, last_upload_number = 0," code = sqlite3_exec(_databaseConnection,"UPDATE localytics_info SET last_session_number = 0, last_upload_number = 0,"
"last_close_event = null, last_flow_event = null, last_session_start = null, " "last_close_event = null, last_flow_event = null, last_session_start = null, "
"custom_d0 = null, custom_d1 = null, custom_d2 = null, custom_d3 = null", "custom_d0 = null, custom_d1 = null, custom_d2 = null, custom_d3 = null, "
"customer_id = null, queued_close_event_blob = null ",
NULL, NULL, NULL); NULL, NULL, NULL);
} }
if (code == SQLITE_OK) { if (code == SQLITE_OK) {
[self releaseTransaction:t]; [self releaseTransaction:t];
} else { } else {
@ -701,6 +919,82 @@ static LocalyticsDatabase *_sharedLocalyticsDatabase = nil;
return [(NSString *)stringUUID autorelease]; return [(NSString *)stringUUID autorelease];
} }
- (NSString *)customerId {
NSString *customerId = nil;
sqlite3_stmt *selectCustomerId;
sqlite3_prepare_v2(_databaseConnection, "SELECT customer_id FROM localytics_info", -1, &selectCustomerId, NULL);
int code = sqlite3_step(selectCustomerId);
if (code == SQLITE_ROW && sqlite3_column_text(selectCustomerId, 0)) {
customerId = [NSString stringWithUTF8String:(char *)sqlite3_column_text(selectCustomerId, 0)];
}
sqlite3_finalize(selectCustomerId);
return customerId;
}
- (BOOL)setCustomerId:(NSString *)newCustomerId
{
sqlite3_stmt *updateCustomerId;
sqlite3_prepare_v2(_databaseConnection, "UPDATE localytics_info set customer_id = ?", -1, &updateCustomerId, NULL);
sqlite3_bind_text (updateCustomerId, 1, [newCustomerId UTF8String], -1, SQLITE_TRANSIENT);
int code = sqlite3_step(updateCustomerId);
sqlite3_finalize(updateCustomerId);
BOOL success = (code == SQLITE_DONE);
return success;
}
#pragma mark - Safe NSDictionary value methods
- (NSInteger)safeIntegerValueFromDictionary:(NSDictionary *)dict forKey:(NSString *)key
{
NSInteger integerValue = 0;
id value = [dict objectForKey:key];
if ([value isKindOfClass:[NSNumber class]] || [value isKindOfClass:[NSString class]]) {
integerValue = [value integerValue];
} else if ([value isKindOfClass:[NSNull class]]) {
integerValue = 0;
}
return integerValue;
}
- (NSString *)safeStringValueFromDictionary:(NSDictionary *)dict forKey:(NSString *)key
{
NSString *stringValue = nil;
id value = [dict objectForKey:key];
if ([value isKindOfClass:[NSString class]]) {
stringValue = value;
} else if ([value isKindOfClass:[NSNumber class]]) {
stringValue = [value stringValue];
} else if ([value isKindOfClass:[NSNull class]]) {
stringValue = nil;
}
return stringValue;
}
- (NSDictionary *)safeDictionaryFromDictionary:(NSDictionary *)dict forKey:(NSString *)key
{
NSDictionary *dictValue = nil;
id value = [dict objectForKey:key];
if ([value isKindOfClass:[NSDictionary class]]) {
dictValue = value;
}
return dictValue;
}
- (NSArray *)safeListFromDictionary:(NSDictionary *)dict forKey:(NSString *)key
{
NSArray *arrayValue = nil;
id value = [dict objectForKey:key];
if ([value isKindOfClass:[NSArray class]]) {
arrayValue = value;
}
return arrayValue;
}
#pragma mark - Lifecycle #pragma mark - Lifecycle
+ (id)allocWithZone:(NSZone *)zone { + (id)allocWithZone:(NSZone *)zone {

View File

@ -1,5 +1,5 @@
// LocalyticsSession.h // LocalyticsSession.h
// Copyright (C) 2009 Char Software Inc., DBA Localytics // Copyright (C) 2012 Char Software Inc., DBA Localytics
// //
// This code is provided under the Localytics Modified BSD License. // This code is provided under the Localytics Modified BSD License.
// A copy of this license has been distributed in a file called LICENSE // A copy of this license has been distributed in a file called LICENSE
@ -8,6 +8,7 @@
// Please visit www.localytics.com for more information. // Please visit www.localytics.com for more information.
#import <UIKit/UIKit.h> #import <UIKit/UIKit.h>
#import <CoreLocation/CoreLocation.h>
// Set this to true to enable localytics traces (useful for debugging) // Set this to true to enable localytics traces (useful for debugging)
#define DO_LOCALYTICS_LOGGING false #define DO_LOCALYTICS_LOGGING false
@ -38,6 +39,7 @@
@author Localytics @author Localytics
*/ */
@interface LocalyticsSession : NSObject { @interface LocalyticsSession : NSObject {
BOOL _hasInitialized; // Whether or not the session object has been initialized. BOOL _hasInitialized; // Whether or not the session object has been initialized.
@ -63,12 +65,24 @@
BOOL _sessionHasBeenOpen; // Whether or not this session has ever been open. BOOL _sessionHasBeenOpen; // Whether or not this session has ever been open.
} }
@property dispatch_queue_t queue; @property (nonatomic,readonly) dispatch_queue_t queue;
@property dispatch_group_t criticalGroup; @property (nonatomic,readonly) dispatch_group_t criticalGroup;
@property BOOL isSessionOpen; @property BOOL isSessionOpen;
@property BOOL hasInitialized; @property BOOL hasInitialized;
@property float backgroundSessionTimeout; @property float backgroundSessionTimeout;
- (void)logMessage:(NSString *)message;
@property (nonatomic, assign, readonly) NSTimeInterval lastSessionStartTimestamp;
@property (nonatomic, assign, readonly) NSInteger sessionNumber;
/*!
@property enableHTTPS
@abstract Determines whether or not HTTPS is used when calling the Localytics
post URL. The default is NO.
*/
@property (nonatomic, assign) BOOL enableHTTPS; // Defaults to NO.
#pragma mark Public Methods #pragma mark Public Methods
/*! /*!
@method sharedLocalyticsSession @method sharedLocalyticsSession
@ -140,9 +154,11 @@
closed and the time of closing is recorded. When the app returns to the foreground, the session closed and the time of closing is recorded. When the app returns to the foreground, the session
is resumed. If the time since closing is greater than BACKGROUND_SESSION_TIMEOUT, (15 seconds is resumed. If the time since closing is greater than BACKGROUND_SESSION_TIMEOUT, (15 seconds
by default) a new session is created, and uploading is triggered. Otherwise, the previous session by default) a new session is created, and uploading is triggered. Otherwise, the previous session
is reopened. is reopened. It is possible to use the return value to determine whether or not a session was resumed.
*/ This may be useful to some customers looking to do conditional instrumentation at the close of a session.
- (void)resume; It is perfectly reasonable to ignore the return value.
@result YES if the sesion was resumed NO if it wasn't (suggesting a new session was created instead).*/
- (BOOL)resume;
/*! /*!
@method close @method close
@ -213,4 +229,23 @@
*/ */
- (void)setCustomDimension:(int)dimension value:(NSString *)value; - (void)setCustomDimension:(int)dimension value:(NSString *)value;
/*!
@method setLocation
@abstract Stores the user's location. This will be used in all event and session calls.
If your application has already collected the user's location, it may be passed to Localytics
via this function. This will cause all events and the session close to include the locatin
information. It is not required that you call this function.
@param deviceLocation The user's location.
*/
- (void)setLocation:(CLLocationCoordinate2D)deviceLocation;
/*!
@method ampTrigger
@abstract Displays the AMP message for the specific event.
Is a stub implementation here to prevent crashes if this class is accidentally used inplace of
the LocalyticsAmpSession
@param event Name of the event.
*/
- (void)ampTrigger:(NSString *)event;
@end @end

View File

@ -1,5 +1,5 @@
// LocalyticsSession.m // LocalyticsSession.m
// Copyright (C) 2009 Char Software Inc., DBA Localytics // Copyright (C) 2012 Char Software Inc., DBA Localytics
// //
// This code is provided under the Localytics Modified BSD License. // This code is provided under the Localytics Modified BSD License.
// A copy of this license has been distributed in a file called LICENSE // A copy of this license has been distributed in a file called LICENSE
@ -22,7 +22,7 @@
#pragma mark Constants #pragma mark Constants
#define PREFERENCES_KEY @"_localytics_install_id" // The randomly generated ID for each install of the app #define PREFERENCES_KEY @"_localytics_install_id" // The randomly generated ID for each install of the app
#define CLIENT_VERSION @"iOS_2.6" // The version of this library #define CLIENT_VERSION @"iOS_2.12" // The version of this library
#define LOCALYTICS_DIR @".localytics" // The directory in which the Localytics database is stored #define LOCALYTICS_DIR @".localytics" // The directory in which the Localytics database is stored
#define IFT_ETHER 0x6 // Ethernet CSMACD #define IFT_ETHER 0x6 // Ethernet CSMACD
#define PATH_TO_APT @"/private/var/lib/apt/" #define PATH_TO_APT @"/private/var/lib/apt/"
@ -44,6 +44,7 @@ static LocalyticsSession *_sharedLocalyticsSession = nil;
@property (nonatomic, retain) NSMutableString *screens; @property (nonatomic, retain) NSMutableString *screens;
@property (nonatomic, assign) NSTimeInterval sessionActiveDuration; @property (nonatomic, assign) NSTimeInterval sessionActiveDuration;
@property (nonatomic, assign) BOOL sessionHasBeenOpen; @property (nonatomic, assign) BOOL sessionHasBeenOpen;
@property (nonatomic, assign) NSInteger sessionNumber;
// Private methods. // Private methods.
- (void)ll_open; - (void)ll_open;
@ -56,22 +57,23 @@ static LocalyticsSession *_sharedLocalyticsSession = nil;
- (BOOL)saveApplicationFlowAndRemoveOnResume:(BOOL)removeOnResume; - (BOOL)saveApplicationFlowAndRemoveOnResume:(BOOL)removeOnResume;
- (NSString *)formatAttributeWithName:(NSString *)paramName value:(NSString *)paramValue; - (NSString *)formatAttributeWithName:(NSString *)paramName value:(NSString *)paramValue;
- (NSString *)formatAttributeWithName:(NSString *)paramName value:(NSString *)paramValue first:(BOOL)firstAttribute; - (NSString *)formatAttributeWithName:(NSString *)paramName value:(NSString *)paramValue first:(BOOL)firstAttribute;
- (void)logMessage:(NSString *)message; -(void) uploadCallback:(NSDictionary*)info;
// Datapoint methods. // Datapoint methods.
- (NSString *)customDimensions; - (NSString *)customDimensions;
- (NSString *)macAddress; - (NSString *)locationDimensions;
- (NSString *)hashString:(NSString *)input; - (NSString *)hashString:(NSString *)input;
- (NSString *)randomUUID; - (NSString *)randomUUID;
- (NSString *)escapeString:(NSString *)input; - (NSString *)escapeString:(NSString *)input;
- (NSString *)installationId; - (NSString *)installationId;
- (NSString *)uniqueDeviceIdentifier;
- (NSString *)appVersion; - (NSString *)appVersion;
- (NSTimeInterval)currentTimestamp; - (NSTimeInterval)currentTimestamp;
- (BOOL)isDeviceJailbroken; - (BOOL)isDeviceJailbroken;
- (NSString *)deviceModel; - (NSString *)deviceModel;
- (NSString *)modelSizeString; - (NSString *)modelSizeString;
- (double)availableMemory; - (double)availableMemory;
- (NSString *)advertisingIdentifier;
- (NSString *)uniqueDeviceIdentifier;
@end @end
@ -92,6 +94,11 @@ static LocalyticsSession *_sharedLocalyticsSession = nil;
@synthesize screens = _screens; @synthesize screens = _screens;
@synthesize sessionActiveDuration = _sessionActiveDuration; @synthesize sessionActiveDuration = _sessionActiveDuration;
@synthesize sessionHasBeenOpen = _sessionHasBeenOpen; @synthesize sessionHasBeenOpen = _sessionHasBeenOpen;
@synthesize sessionNumber = _sessionNumber;
@synthesize enableHTTPS = _enableHTTPS;
// Stores the last location passed in to the app.
CLLocationCoordinate2D lastDeviceLocation = {0};
#pragma mark Singleton #pragma mark Singleton
@ -112,6 +119,7 @@ static LocalyticsSession *_sharedLocalyticsSession = nil;
_sessionHasBeenOpen = NO; _sessionHasBeenOpen = NO;
_queue = dispatch_queue_create("com.Localytics.operations", DISPATCH_QUEUE_SERIAL); _queue = dispatch_queue_create("com.Localytics.operations", DISPATCH_QUEUE_SERIAL);
_criticalGroup = dispatch_group_create(); _criticalGroup = dispatch_group_create();
_enableHTTPS = NO;
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationDidEnterBackground:) name:UIApplicationDidEnterBackgroundNotification object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationDidEnterBackground:) name:UIApplicationDidEnterBackgroundNotification object:nil];
@ -171,6 +179,15 @@ static LocalyticsSession *_sharedLocalyticsSession = nil;
} }
- (void)startSession:(NSString *)appKey { - (void)startSession:(NSString *)appKey {
//check app key
NSPredicate *matchPred = [NSPredicate predicateWithFormat:@"SELF MATCHES %@", @"[A-Fa-f0-9-]+"];
BOOL matches = [matchPred evaluateWithObject:appKey];
if (matches == NO) {
//generate exception
NSException *exception = [NSException exceptionWithName:@"Invalid Localytics App Key" reason:@"Application key is not valid. Please look at the iOS integration guidlines at http://www.localytics.com/docs/iphone-integration/" userInfo:nil];
[exception raise];
}
[self LocalyticsSession:appKey]; [self LocalyticsSession:appKey];
[self open]; [self open];
[self upload]; [self upload];
@ -183,35 +200,48 @@ static LocalyticsSession *_sharedLocalyticsSession = nil;
}); });
} }
- (void)resume {
dispatch_async(_queue, ^{
// Do nothing if session is already open
if(self.isSessionOpen == YES)
return;
if([self ll_isOptedIn] == false) { - (BOOL)resume {
[self logMessage:@"Can't resume session because user is opted out."]; __block BOOL resumed = NO;
return;
}
// conditions for resuming previous session dispatch_sync(_queue,^{
if(self.sessionHasBeenOpen && @try {
(!self.sessionCloseTime || // Do nothing if session is already open
[self.sessionCloseTime timeIntervalSinceNow]*-1 <= self.backgroundSessionTimeout)) { if(self.isSessionOpen == YES) {
// Note that we allow the session to be resumed even if the database size exceeds the resumed = YES;
// maximum. This is because we don't want to create incomplete sessions. If the DB was large return;
// enough that the previous session could not be opened, there will be nothing to resume. But }
// if this session caused it to go over it is better to let it complete and stop the next one
// from being created. if([self ll_isOptedIn] == false) {
[self logMessage:@"Resume called - Resuming previous session."]; [self logMessage:@"Can't resume session because user is opted out."];
[self reopenPreviousSession]; resumed = NO;
} else { return;
// otherwise open new session and upload }
[self logMessage:@"Resume called - Opening a new session."];
[self ll_open]; // conditions for resuming previous session
} if(self.sessionHasBeenOpen &&
self.sessionCloseTime = nil; (!self.sessionCloseTime ||
}); [self.sessionCloseTime timeIntervalSinceNow]*-1 <= self.backgroundSessionTimeout)) {
// Note that we allow the session to be resumed even if the database size exceeds the
// maximum. This is because we don't want to create incomplete sessions. If the DB was large
// enough that the previous session could not be opened, there will be nothing to resume. But
// if this session caused it to go over it is better to let it complete and stop the next one
// from being created.
[self logMessage:@"Resume called - Resuming previous session."];
[self reopenPreviousSession];
resumed = YES;
} else {
// otherwise open new session and upload
[self logMessage:@"Resume called - Opening a new session."];
[self ll_open];
resumed = NO;
}
self.sessionCloseTime = nil;
} @catch (NSException *e) {}
});
return resumed;
} }
- (void)close { - (void)close {
@ -237,13 +267,13 @@ static LocalyticsSession *_sharedLocalyticsSession = nil;
[closeEventString appendString:[self formatAttributeWithName:PARAM_DATA_TYPE value:@"c" first:YES]]; [closeEventString appendString:[self formatAttributeWithName:PARAM_DATA_TYPE value:@"c" first:YES]];
[closeEventString appendString:[self formatAttributeWithName:PARAM_SESSION_UUID value:self.sessionUUID]]; [closeEventString appendString:[self formatAttributeWithName:PARAM_SESSION_UUID value:self.sessionUUID]];
[closeEventString appendString:[self formatAttributeWithName:PARAM_UUID value:[self randomUUID] ]]; [closeEventString appendString:[self formatAttributeWithName:PARAM_UUID value:[self randomUUID] ]];
[closeEventString appendFormat:@",\"%@\":%u", PARAM_SESSION_START, (long)self.lastSessionStartTimestamp]; [closeEventString appendFormat:@",\"%@\":%ld", PARAM_SESSION_START, (long)self.lastSessionStartTimestamp];
[closeEventString appendFormat:@",\"%@\":%u", PARAM_SESSION_ACTIVE, (long)self.sessionActiveDuration]; [closeEventString appendFormat:@",\"%@\":%ld", PARAM_SESSION_ACTIVE, (long)self.sessionActiveDuration];
[closeEventString appendFormat:@",\"%@\":%u", PARAM_CLIENT_TIME, (long)[self currentTimestamp]]; [closeEventString appendFormat:@",\"%@\":%ld", PARAM_CLIENT_TIME, (long)[self currentTimestamp]];
// Avoid recording session lengths of users with unreasonable client times (usually caused by developers testing clock change attacks) // Avoid recording session lengths of users with unreasonable client times (usually caused by developers testing clock change attacks)
if(sessionLength > 0 && sessionLength < 400000) { if(sessionLength > 0 && sessionLength < 400000) {
[closeEventString appendFormat:@",\"%@\":%u", PARAM_SESSION_TOTAL, sessionLength]; [closeEventString appendFormat:@",\"%@\":%d", PARAM_SESSION_TOTAL, sessionLength];
} }
// Open second level - screen flow // Open second level - screen flow
@ -256,19 +286,16 @@ static LocalyticsSession *_sharedLocalyticsSession = nil;
// Append the custom dimensions // Append the custom dimensions
[closeEventString appendString:[self customDimensions]]; [closeEventString appendString:[self customDimensions]];
// Append the location
[closeEventString appendString:[self locationDimensions]];
// Close first level - close blob // Close first level - close blob
[closeEventString appendString:@"}\n"]; [closeEventString appendString:@"}\n"];
BOOL success = [[LocalyticsDatabase sharedLocalyticsDatabase] addCloseEventWithBlobString:[[closeEventString copy] autorelease]]; BOOL success = [[LocalyticsDatabase sharedLocalyticsDatabase] queueCloseEventWithBlobString:[[closeEventString copy] autorelease]];
self.isSessionOpen = NO; // Session is no longer open. self.isSessionOpen = NO; // Session is no longer open.
if (success) {
// Record final session flow, opting to remove it from the database if the session happens to resume.
// This is safe now that the session has closed because no new events can be added.
success = [self saveApplicationFlowAndRemoveOnResume:YES];
}
if (success) { if (success) {
[self logMessage:@"Session succesfully closed."]; [self logMessage:@"Session succesfully closed."];
} else { } else {
@ -358,11 +385,14 @@ static LocalyticsSession *_sharedLocalyticsSession = nil;
[eventString appendString:[self formatAttributeWithName:PARAM_APP_KEY value:self.applicationKey ]]; [eventString appendString:[self formatAttributeWithName:PARAM_APP_KEY value:self.applicationKey ]];
[eventString appendString:[self formatAttributeWithName:PARAM_SESSION_UUID value:self.sessionUUID ]]; [eventString appendString:[self formatAttributeWithName:PARAM_SESSION_UUID value:self.sessionUUID ]];
[eventString appendString:[self formatAttributeWithName:PARAM_EVENT_NAME value:[self escapeString:event] ]]; [eventString appendString:[self formatAttributeWithName:PARAM_EVENT_NAME value:[self escapeString:event] ]];
[eventString appendFormat:@",\"%@\":%u", PARAM_CLIENT_TIME, (long)[self currentTimestamp]]; [eventString appendFormat:@",\"%@\":%ld", PARAM_CLIENT_TIME, (long)[self currentTimestamp]];
// Append the custom dimensions // Append the custom dimensions
[eventString appendString:[self customDimensions]]; [eventString appendString:[self customDimensions]];
// Append the location
[eventString appendString:[self locationDimensions]];
// If there are any attributes for this event, add them as a hash // If there are any attributes for this event, add them as a hash
int attrIndex = 0; int attrIndex = 0;
if(attributes != nil) if(attributes != nil)
@ -406,6 +436,8 @@ static LocalyticsSession *_sharedLocalyticsSession = nil;
// User-originated events should be tracked as application flow. // User-originated events should be tracked as application flow.
[self addFlowEventWithName:event type:@"e"]; // "e" for Event. [self addFlowEventWithName:event type:@"e"]; // "e" for Event.
[self ampTrigger:event];
[self logMessage:[@"Tagged event: " stringByAppendingString:event]]; [self logMessage:[@"Tagged event: " stringByAppendingString:event]];
} else { } else {
[self logMessage:@"Failed to tag event."]; [self logMessage:@"Failed to tag event."];
@ -437,6 +469,11 @@ static LocalyticsSession *_sharedLocalyticsSession = nil;
}); });
} }
- (void)setLocation:(CLLocationCoordinate2D)deviceLocation {
lastDeviceLocation = deviceLocation;
[self logMessage:@"Setting Location"];
}
- (void)setCustomDimension:(int)dimension value:(NSString *)value { - (void)setCustomDimension:(int)dimension value:(NSString *)value {
dispatch_async(_queue, ^{ dispatch_async(_queue, ^{
if(dimension < 0 || dimension > 3) { if(dimension < 0 || dimension > 3) {
@ -503,7 +540,7 @@ static LocalyticsSession *_sharedLocalyticsSession = nil;
} }
// Begin upload. // Begin upload.
[[LocalyticsUploader sharedLocalyticsUploader] uploaderWithApplicationKey:self.applicationKey]; [[LocalyticsUploader sharedLocalyticsUploader] uploaderWithApplicationKey:self.applicationKey useHTTPS:[self enableHTTPS] installId:[self installationId] resultTarget:self callback:@selector(uploadCallback:)];
} else { } else {
[db rollbackTransaction:t]; [db rollbackTransaction:t];
[self logMessage:@"Failed to start upload."]; [self logMessage:@"Failed to start upload."];
@ -515,6 +552,26 @@ static LocalyticsSession *_sharedLocalyticsSession = nil;
#pragma mark Private Methods #pragma mark Private Methods
-(NSString*)libraryVersion {
return CLIENT_VERSION;
}
-(void) uploadCallback:(NSDictionary*)info{
}
- (void)dequeueCloseEventBlobString
{
LocalyticsDatabase *db = [LocalyticsDatabase sharedLocalyticsDatabase];
NSString *closeEventString = [db dequeueCloseEventBlobString];
if (closeEventString) {
BOOL success = [db addCloseEventWithBlobString:closeEventString];
if (!success) {
// Re-queue the close event.
[db queueCloseEventWithBlobString:closeEventString];
}
}
}
- (void)ll_open { - (void)ll_open {
// There are a number of conditions in which nothing should be done: // There are a number of conditions in which nothing should be done:
if (self.hasInitialized == NO || // the session object has not yet initialized if (self.hasInitialized == NO || // the session object has not yet initialized
@ -538,6 +595,8 @@ static LocalyticsSession *_sharedLocalyticsSession = nil;
return; return;
} }
[self dequeueCloseEventBlobString];
self.sessionActiveDuration = 0; self.sessionActiveDuration = 0;
self.sessionResumeTime = [NSDate date]; self.sessionResumeTime = [NSDate date];
self.unstagedFlowEvents = [NSMutableString string]; self.unstagedFlowEvents = [NSMutableString string];
@ -548,6 +607,12 @@ static LocalyticsSession *_sharedLocalyticsSession = nil;
NSString *t = @"open_session"; NSString *t = @"open_session";
BOOL success = [db beginTransaction:t]; BOOL success = [db beginTransaction:t];
// lastSessionStartTimestamp isn't really the last session start time.
// It's the sessionResumeTime which is [NSDate date] or now. Therefore,
// save the current lastSessionTimestamp value from the database so it
// can be used to calculate the elapsed time between session start times.
NSTimeInterval previousSessionStartTimeInterval = [db lastSessionStartTimestamp];
// Save session start time. // Save session start time.
self.lastSessionStartTimestamp = [self.sessionResumeTime timeIntervalSince1970]; self.lastSessionStartTimestamp = [self.sessionResumeTime timeIntervalSince1970];
if (success) { if (success) {
@ -559,6 +624,7 @@ static LocalyticsSession *_sharedLocalyticsSession = nil;
if (success) { if (success) {
success = [db incrementLastSessionNumber:&sessionNumber]; success = [db incrementLastSessionNumber:&sessionNumber];
} }
[self setSessionNumber:sessionNumber];
if (success) { if (success) {
// Prepare session open event. // Prepare session open event.
@ -569,10 +635,18 @@ static LocalyticsSession *_sharedLocalyticsSession = nil;
[openEventString appendString:@"{"]; [openEventString appendString:@"{"];
[openEventString appendString:[self formatAttributeWithName:PARAM_DATA_TYPE value:@"s" first:YES]]; [openEventString appendString:[self formatAttributeWithName:PARAM_DATA_TYPE value:@"s" first:YES]];
[openEventString appendString:[self formatAttributeWithName:PARAM_NEW_SESSION_UUID value:self.sessionUUID]]; [openEventString appendString:[self formatAttributeWithName:PARAM_NEW_SESSION_UUID value:self.sessionUUID]];
[openEventString appendFormat:@",\"%@\":%u", PARAM_CLIENT_TIME, (long)self.lastSessionStartTimestamp]; [openEventString appendFormat:@",\"%@\":%ld", PARAM_CLIENT_TIME, (long)self.lastSessionStartTimestamp];
[openEventString appendFormat:@",\"%@\":%d", PARAM_SESSION_NUMBER, sessionNumber]; [openEventString appendFormat:@",\"%@\":%d", PARAM_SESSION_NUMBER, sessionNumber];
double elapsedTime = 0.0;
if (previousSessionStartTimeInterval > 0) {
elapsedTime = [self lastSessionStartTimestamp] - previousSessionStartTimeInterval;
}
NSString *elapsedTimeString = [NSString stringWithFormat:@"%.0f", elapsedTime];
[openEventString appendString:[self formatAttributeWithName:PARAM_SESSION_ELAPSE_TIME value:elapsedTimeString]];
[openEventString appendString:[self customDimensions]]; [openEventString appendString:[self customDimensions]];
[openEventString appendString:[self locationDimensions]];
[openEventString appendString:@"}\n"]; [openEventString appendString:@"}\n"];
@ -667,12 +741,13 @@ static LocalyticsSession *_sharedLocalyticsSession = nil;
NSString *device_language = [english displayNameForKey:NSLocaleIdentifier value:device_locale]; NSString *device_language = [english displayNameForKey:NSLocaleIdentifier value:device_locale];
NSString *locale_country = [english displayNameForKey:NSLocaleCountryCode value:[locale objectForKey:NSLocaleCountryCode]]; NSString *locale_country = [english displayNameForKey:NSLocaleCountryCode value:[locale objectForKey:NSLocaleCountryCode]];
NSString *uuid = [self randomUUID]; NSString *uuid = [self randomUUID];
NSString *device_uuid = [self uniqueDeviceIdentifier]; NSString *device_uuid = [self uniqueDeviceIdentifier];
NSString *device_adid = [self advertisingIdentifier];
// Open first level - blob information // Open first level - blob information
[headerString appendString:@"{"]; [headerString appendString:@"{"];
[headerString appendFormat:@"\"%@\":%d", PARAM_SEQUENCE_NUMBER, nextSequenceNumber]; [headerString appendFormat:@"\"%@\":%d", PARAM_SEQUENCE_NUMBER, nextSequenceNumber];
[headerString appendFormat:@",\"%@\":%u", PARAM_PERSISTED_AT, (long)[[LocalyticsDatabase sharedLocalyticsDatabase] createdTimestamp]]; [headerString appendFormat:@",\"%@\":%ld", PARAM_PERSISTED_AT, (long)[[LocalyticsDatabase sharedLocalyticsDatabase] createdTimestamp]];
[headerString appendString:[self formatAttributeWithName:PARAM_DATA_TYPE value:@"h" ]]; [headerString appendString:[self formatAttributeWithName:PARAM_DATA_TYPE value:@"h" ]];
[headerString appendString:[self formatAttributeWithName:PARAM_UUID value:uuid ]]; [headerString appendString:[self formatAttributeWithName:PARAM_UUID value:uuid ]];
@ -684,18 +759,22 @@ static LocalyticsSession *_sharedLocalyticsSession = nil;
[headerString appendString:[self formatAttributeWithName:PARAM_INSTALL_ID value:[self installationId] ]]; [headerString appendString:[self formatAttributeWithName:PARAM_INSTALL_ID value:[self installationId] ]];
[headerString appendString:[self formatAttributeWithName:PARAM_APP_KEY value:self.applicationKey ]]; [headerString appendString:[self formatAttributeWithName:PARAM_APP_KEY value:self.applicationKey ]];
[headerString appendString:[self formatAttributeWithName:PARAM_APP_VERSION value:[self appVersion] ]]; [headerString appendString:[self formatAttributeWithName:PARAM_APP_VERSION value:[self appVersion] ]];
[headerString appendString:[self formatAttributeWithName:PARAM_LIBRARY_VERSION value:CLIENT_VERSION ]]; [headerString appendString:[self formatAttributeWithName:PARAM_LIBRARY_VERSION value:[self libraryVersion] ]];
// >> Device Information // >> Device Information
// [headerString appendString:[self formatAttributeWithName:PARAM_DEVICE_UUID value:device_uuid ]]; if (device_uuid) {
[headerString appendString:[self formatAttributeWithName:PARAM_DEVICE_UUID_HASHED value:[self hashString:device_uuid] ]]; [headerString appendString:[self formatAttributeWithName:PARAM_DEVICE_UUID_HASHED value:[self hashString:device_uuid] ]];
}
if (device_adid) {
[headerString appendString:[self formatAttributeWithName:PARAM_DEVICE_ADID value:device_adid]];
}
[headerString appendString:[self formatAttributeWithName:PARAM_DEVICE_PLATFORM value:[thisDevice model] ]]; [headerString appendString:[self formatAttributeWithName:PARAM_DEVICE_PLATFORM value:[thisDevice model] ]];
[headerString appendString:[self formatAttributeWithName:PARAM_DEVICE_OS_VERSION value:[thisDevice systemVersion] ]]; [headerString appendString:[self formatAttributeWithName:PARAM_DEVICE_OS_VERSION value:[thisDevice systemVersion] ]];
[headerString appendString:[self formatAttributeWithName:PARAM_DEVICE_MODEL value:[self deviceModel] ]]; [headerString appendString:[self formatAttributeWithName:PARAM_DEVICE_MODEL value:[self deviceModel] ]];
// MAC Address collection. Uncomment the following line to add Mac address to the mix of collected identifiers // MAC Address collection. Uncomment the following line to add Mac address to the mix of collected identifiers
// [headerString appendString:[self formatAttributeWithName:PARAM_DEVICE_MAC value:[self hashString:[self macAddress]] ]]; // [headerString appendString:[self formatAttributeWithName:PARAM_DEVICE_MAC value:[self hashString:[self macAddress]] ]];
[headerString appendString:[NSString stringWithFormat:@",\"%@\":%d", PARAM_DEVICE_MEMORY, (long)[self availableMemory] ]]; [headerString appendString:[NSString stringWithFormat:@",\"%@\":%ld", PARAM_DEVICE_MEMORY, (long)[self availableMemory] ]];
[headerString appendString:[self formatAttributeWithName:PARAM_LOCALE_LANGUAGE value:device_language]]; [headerString appendString:[self formatAttributeWithName:PARAM_LOCALE_LANGUAGE value:device_language]];
[headerString appendString:[self formatAttributeWithName:PARAM_LOCALE_COUNTRY value:locale_country]]; [headerString appendString:[self formatAttributeWithName:PARAM_LOCALE_COUNTRY value:locale_country]];
[headerString appendString:[self formatAttributeWithName:PARAM_DEVICE_COUNTRY value:[locale objectForKey:NSLocaleCountryCode]]]; [headerString appendString:[self formatAttributeWithName:PARAM_DEVICE_COUNTRY value:[locale objectForKey:NSLocaleCountryCode]]];
@ -723,9 +802,9 @@ static LocalyticsSession *_sharedLocalyticsSession = nil;
NSMutableString *optEventString = [NSMutableString string]; NSMutableString *optEventString = [NSMutableString string];
[optEventString appendString:@"{"]; [optEventString appendString:@"{"];
[optEventString appendString:[self formatAttributeWithName:PARAM_DATA_TYPE value:@"o" first:YES]]; [optEventString appendString:[self formatAttributeWithName:PARAM_DATA_TYPE value:@"o" first:YES]];
[optEventString appendString:[self formatAttributeWithName:PARAM_APP_KEY value:self.applicationKey first:NO]]; [optEventString appendString:[self formatAttributeWithName:PARAM_UUID value:[self randomUUID] first:NO ]];
[optEventString appendString:[NSString stringWithFormat:@",\"%@\":%@", PARAM_OPT_VALUE, (optState ? @"true" : @"false") ]]; [optEventString appendString:[NSString stringWithFormat:@",\"%@\":%@", PARAM_OPT_VALUE, (optState ? @"false" : @"true") ]]; //this actually transmits the opposite of the opt state. The JSON contains whether the user is opted out, not whether the user is opted in.
[optEventString appendFormat:@",\"%@\":%u", PARAM_CLIENT_TIME, (long)[self currentTimestamp]]; [optEventString appendFormat:@",\"%@\":%ld", PARAM_CLIENT_TIME, (long)[self currentTimestamp]];
[optEventString appendString:@"}\n"]; [optEventString appendString:@"}\n"];
BOOL success = [[LocalyticsDatabase sharedLocalyticsDatabase] addEventWithBlobString:[[optEventString copy] autorelease]]; BOOL success = [[LocalyticsDatabase sharedLocalyticsDatabase] addEventWithBlobString:[[optEventString copy] autorelease]];
@ -752,7 +831,7 @@ static LocalyticsSession *_sharedLocalyticsSession = nil;
[flowEventString appendString:@"{"]; [flowEventString appendString:@"{"];
[flowEventString appendString:[self formatAttributeWithName:PARAM_DATA_TYPE value:@"f" first:YES]]; [flowEventString appendString:[self formatAttributeWithName:PARAM_DATA_TYPE value:@"f" first:YES]];
[flowEventString appendString:[self formatAttributeWithName:PARAM_UUID value:[self randomUUID] ]]; [flowEventString appendString:[self formatAttributeWithName:PARAM_UUID value:[self randomUUID] ]];
[flowEventString appendFormat:@",\"%@\":%u", PARAM_SESSION_START, (long)self.lastSessionStartTimestamp]; [flowEventString appendFormat:@",\"%@\":%ld", PARAM_SESSION_START, (long)self.lastSessionStartTimestamp];
// Open second level - new flow events // Open second level - new flow events
[flowEventString appendFormat:@",\"%@\":[", PARAM_NEW_FLOW_EVENTS]; [flowEventString appendFormat:@",\"%@\":[", PARAM_NEW_FLOW_EVENTS];
@ -814,8 +893,14 @@ static LocalyticsSession *_sharedLocalyticsSession = nil;
{ {
NSString *output = [input stringByReplacingOccurrencesOfString:@"\\" withString:@"\\\\"]; NSString *output = [input stringByReplacingOccurrencesOfString:@"\\" withString:@"\\\\"];
output = [output stringByReplacingOccurrencesOfString:@"\"" withString:@"\\\""]; output = [output stringByReplacingOccurrencesOfString:@"\"" withString:@"\\\""];
output = [output stringByReplacingOccurrencesOfString:@"\n" withString:@"\\n"]; output = [output stringByReplacingOccurrencesOfString:@"\'" withString:@"\\\'"];
return output; output = [output stringByReplacingOccurrencesOfString:@"\n" withString:@"\\n"];
output = [output stringByReplacingOccurrencesOfString:@"\t" withString:@"\\t"];
output = [output stringByReplacingOccurrencesOfString:@"\b" withString:@"\\b"];
output = [output stringByReplacingOccurrencesOfString:@"\r" withString:@"\\r"];
output = [output stringByReplacingOccurrencesOfString:@"\f" withString:@"\\f"];
output = [output stringByReplacingOccurrencesOfString:@"\v" withString:@"\\v"];
return output;
} }
- (void)applicationDidEnterBackground:(NSNotification *)notification - (void)applicationDidEnterBackground:(NSNotification *)notification
@ -877,6 +962,24 @@ static LocalyticsSession *_sharedLocalyticsSession = nil;
return [[dimensions copy] autorelease]; return [[dimensions copy] autorelease];
} }
/*!
@method locationDimensions
@abstract Returns the json blob containing the current location if available or nil if no location is available.
*/
- (NSString *)locationDimensions
{
if(lastDeviceLocation.latitude == 0 || lastDeviceLocation.longitude == 0) {
return @"";
}
return [NSString stringWithFormat:@",\"lat\":%f,\"lng\":%f",
lastDeviceLocation.latitude,
lastDeviceLocation.longitude];
return [NSString stringWithFormat:@"%lf", lastDeviceLocation.latitude];
}
/*! /*!
@method macAddress @method macAddress
@abstract Returns the macAddress of this device. @abstract Returns the macAddress of this device.
@ -983,6 +1086,7 @@ static LocalyticsSession *_sharedLocalyticsSession = nil;
return installId; return installId;
} }
/*! /*!
@method uniqueDeviceIdentifier @method uniqueDeviceIdentifier
@abstract A unique device identifier is a hash value composed from various hardware identifiers such @abstract A unique device identifier is a hash value composed from various hardware identifiers such
@ -992,18 +1096,38 @@ static LocalyticsSession *_sharedLocalyticsSession = nil;
*/ */
- (NSString *)uniqueDeviceIdentifier { - (NSString *)uniqueDeviceIdentifier {
// Supress the warning for uniqueIdentifier being deprecated. NSString *systemId = nil;
// We collect it as long as it is available along with a randomly generated ID. // We collect it as long as it is available along with a randomly generated ID.
// This way, when this becomes unavailable we can map existing users so the // This way, when this becomes unavailable we can map existing users so the
// new vs returning counts do not break. This will be removed before it causes grief. // new vs returning counts do not break.
#pragma clang diagnostic push //only do this if the OS is less than 6.0
#pragma clang diagnostic ignored "-Wdeprecated-declarations" if (([[[UIDevice currentDevice] systemVersion] floatValue] < 6.0f)) {
NSString *systemId = [[UIDevice currentDevice] uniqueIdentifier]; SEL udidSelector = NSSelectorFromString(@"uniqueIdentifier");
#pragma clang diagnostic pop if ([[UIDevice currentDevice] respondsToSelector:udidSelector]) {
systemId = [[UIDevice currentDevice] performSelector:udidSelector];
}
}
return systemId; return systemId;
} }
/*!
@method advertisingIdentifier
@abstract An alphanumeric string unique to each device, used for advertising only.
From UIDevice documentation.
@return An identifier unique to this device.
*/
- (NSString *)advertisingIdentifier {
NSString *adId = nil;
SEL adidSelector = NSSelectorFromString(@"identifierForAdvertising");
if ([[UIDevice currentDevice] respondsToSelector:adidSelector]) {
adId = [[[UIDevice currentDevice] performSelector:adidSelector] performSelector:NSSelectorFromString(@"UUIDString")];
}
return adId;
}
/*! /*!
@method appVersion @method appVersion
@abstract Gets the pretty string for this application's version. @abstract Gets the pretty string for this application's version.
@ -1145,4 +1269,10 @@ static LocalyticsSession *_sharedLocalyticsSession = nil;
[super dealloc]; [super dealloc];
} }
#pragma mark - AMP stub
- (void)ampTrigger:(NSString *)event {
//do nothing
}
@end @end

View File

@ -1,5 +1,5 @@
// LocalyticsUploader.h // LocalyticsUploader.h
// Copyright (C) 2009 Char Software Inc., DBA Localytics // Copyright (C) 2012 Char Software Inc., DBA Localytics
// //
// This code is provided under the Localytics Modified BSD License. // This code is provided under the Localytics Modified BSD License.
// A copy of this license has been distributed in a file called LICENSE // A copy of this license has been distributed in a file called LICENSE
@ -9,6 +9,8 @@
#import <UIKit/UIKit.h> #import <UIKit/UIKit.h>
extern NSString * const kLocalyticsKeyResponseBody;
/*! /*!
@class LocalyticsUploader @class LocalyticsUploader
@discussion Singleton class to handle data uploads @discussion Singleton class to handle data uploads
@ -35,8 +37,28 @@
writing data regardless of whether or not the upload succeeds. Files writing data regardless of whether or not the upload succeeds. Files
which have been renamed still count towards the total number of Localytics which have been renamed still count towards the total number of Localytics
files which can be stored on the disk. files which can be stored on the disk.
This version of the method now just calls the second version of it with a nil target and NULL callback method.
@param localyticsApplicationKey the Localytics application ID @param localyticsApplicationKey the Localytics application ID
@param useHTTPS Flag determining whether HTTP or HTTPS is used for the post URL.
@param installId Install id passed to the server in the x-install-id header field.
*/ */
- (void)uploaderWithApplicationKey:(NSString *)localyticsApplicationKey; - (void)uploaderWithApplicationKey:(NSString *)localyticsApplicationKey useHTTPS:(BOOL)useHTTPS installId:(NSString *)installId;
/*!
@method LocalyticsUploader
@abstract Creates a thread which uploads all queued header and event data.
All files starting with sessionFilePrefix are renamed,
uploaded and deleted on upload. This way the sessions can continue
writing data regardless of whether or not the upload succeeds. Files
which have been renamed still count towards the total number of Localytics
files which can be stored on the disk.
@param localyticsApplicationKey the Localytics application ID
@param useHTTPS Flag determining whether HTTP or HTTPS is used for the post URL.
@param installId Install id passed to the server in the x-install-id header field.
@param resultTarget Result target is the target for the callback method that knows how to handle response data
@param callback Callback is the method of the target class that is to be called with the data begin returned by an upload
*/
- (void)uploaderWithApplicationKey:(NSString *)localyticsApplicationKey useHTTPS:(BOOL)useHTTPS installId:(NSString *)installId resultTarget:(id)target callback:(SEL)callbackMethod;
@end @end

View File

@ -1,5 +1,5 @@
// LocalyticsUploader.m // LocalyticsUploader.m
// Copyright (C) 2009 Char Software Inc., DBA Localytics // Copyright (C) 2012 Char Software Inc., DBA Localytics
// //
// This code is provided under the Localytics Modified BSD License. // This code is provided under the Localytics Modified BSD License.
// A copy of this license has been distributed in a file called LICENSE // A copy of this license has been distributed in a file called LICENSE
@ -10,16 +10,25 @@
#import "LocalyticsUploader.h" #import "LocalyticsUploader.h"
#import "LocalyticsSession.h" #import "LocalyticsSession.h"
#import "LocalyticsDatabase.h" #import "LocalyticsDatabase.h"
#import "WebserviceConstants.h"
#import <zlib.h> #import <zlib.h>
#define LOCALYTICS_URL @"http://analytics.localytics.com/api/v2/applications/%@/uploads" #ifndef LOCALYTICS_URL
#define LOCALYTICS_URL @"http://analytics.localytics.com/api/v2/applications/%@/uploads"
#endif
#ifndef LOCALYTICS_URL_SECURED
#define LOCALYTICS_URL_SECURED @"https://analytics.localytics.com/api/v2/applications/%@/uploads"
#endif
static LocalyticsUploader *_sharedUploader = nil; static LocalyticsUploader *_sharedUploader = nil;
NSString * const kLocalyticsKeyResponseBody = @"localytics.key.responseBody";
@interface LocalyticsUploader () @interface LocalyticsUploader ()
- (void)finishUpload; - (void)finishUpload;
- (NSData *)gzipDeflatedDataWithData:(NSData *)data; - (NSData *)gzipDeflatedDataWithData:(NSData *)data;
- (void)logMessage:(NSString *)message; - (void)logMessage:(NSString *)message;
- (NSString *)uploadTimeStamp;
@property (readwrite) BOOL isUploading; @property (readwrite) BOOL isUploading;
@ -40,7 +49,13 @@ static LocalyticsUploader *_sharedUploader = nil;
#pragma mark - Class Methods #pragma mark - Class Methods
- (void)uploaderWithApplicationKey:(NSString *)localyticsApplicationKey { - (void)uploaderWithApplicationKey:(NSString *)localyticsApplicationKey useHTTPS:(BOOL)useHTTPS installId:(NSString *)installId
{
[self uploaderWithApplicationKey:localyticsApplicationKey useHTTPS:useHTTPS installId:installId resultTarget:nil callback:NULL];
}
- (void)uploaderWithApplicationKey:(NSString *)localyticsApplicationKey useHTTPS:(BOOL)useHTTPS installId:(NSString *)installId resultTarget:(id)target callback:(SEL)callbackMethod;
{
// Do nothing if already uploading. // Do nothing if already uploading.
if (self.isUploading == true) if (self.isUploading == true)
@ -77,17 +92,26 @@ static LocalyticsUploader *_sharedUploader = nil;
NSData *requestData = [blobString dataUsingEncoding:NSUTF8StringEncoding]; NSData *requestData = [blobString dataUsingEncoding:NSUTF8StringEncoding];
NSString *myString = [[[NSString alloc] initWithData:requestData encoding:NSUTF8StringEncoding] autorelease]; NSString *myString = [[[NSString alloc] initWithData:requestData encoding:NSUTF8StringEncoding] autorelease];
[self logMessage:[NSString stringWithFormat:@"Uploading data (length: %u)", [myString length]]]; [self logMessage:[NSString stringWithFormat:@"Uploading data (length: %u)", [myString length]]];
[self logMessage:myString];
// Step 2 // Step 2
NSData *deflatedRequestData = [[self gzipDeflatedDataWithData:requestData] retain]; NSData *deflatedRequestData = [[self gzipDeflatedDataWithData:requestData] retain];
[pool drain]; [pool drain];
NSString *apiUrlString = [NSString stringWithFormat:LOCALYTICS_URL, [localyticsApplicationKey stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]]; NSString *urlStringFormat;
if (useHTTPS) {
urlStringFormat = LOCALYTICS_URL_SECURED;
} else {
urlStringFormat = LOCALYTICS_URL;
}
NSString *apiUrlString = [NSString stringWithFormat:urlStringFormat, [localyticsApplicationKey stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]];
NSMutableURLRequest *submitRequest = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:apiUrlString] NSMutableURLRequest *submitRequest = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:apiUrlString]
cachePolicy:NSURLRequestReloadIgnoringCacheData cachePolicy:NSURLRequestReloadIgnoringCacheData
timeoutInterval:60.0]; timeoutInterval:60.0];
[submitRequest setHTTPMethod:@"POST"]; [submitRequest setHTTPMethod:@"POST"];
[submitRequest setValue:[self uploadTimeStamp] forHTTPHeaderField:HEADER_CLIENT_TIME];
[submitRequest setValue:installId forHTTPHeaderField:HEADER_INSTALL_ID];
[submitRequest setValue:@"application/x-gzip" forHTTPHeaderField:@"Content-Type"]; [submitRequest setValue:@"application/x-gzip" forHTTPHeaderField:@"Content-Type"];
[submitRequest setValue:@"gzip" forHTTPHeaderField:@"Content-Encoding"]; [submitRequest setValue:@"gzip" forHTTPHeaderField:@"Content-Encoding"];
[submitRequest setValue:[NSString stringWithFormat:@"%d", [deflatedRequestData length]] forHTTPHeaderField:@"Content-Length"]; [submitRequest setValue:[NSString stringWithFormat:@"%d", [deflatedRequestData length]] forHTTPHeaderField:@"Content-Length"];
@ -100,7 +124,7 @@ static LocalyticsUploader *_sharedUploader = nil;
@try { @try {
NSURLResponse *response = nil; NSURLResponse *response = nil;
NSError *responseError = nil; NSError *responseError = nil;
[NSURLConnection sendSynchronousRequest:submitRequest returningResponse:&response error:&responseError]; NSData *responseData = [NSURLConnection sendSynchronousRequest:submitRequest returningResponse:&response error:&responseError];
NSInteger responseStatusCode = [(NSHTTPURLResponse *)response statusCode]; NSInteger responseStatusCode = [(NSHTTPURLResponse *)response statusCode];
if (responseError) { if (responseError) {
@ -123,6 +147,18 @@ static LocalyticsUploader *_sharedUploader = nil;
[[LocalyticsDatabase sharedLocalyticsDatabase] deleteUploadedData]; [[LocalyticsDatabase sharedLocalyticsDatabase] deleteUploadedData];
} }
} }
if ([responseData length] > 0) {
if (DO_LOCALYTICS_LOGGING) {
NSString *responseString = [[NSString alloc] initWithData:responseData encoding:NSUTF8StringEncoding];
[self logMessage:[NSString stringWithFormat:@"Response body: %@", responseString]];
[responseString release];
}
NSDictionary *userInfo = [NSDictionary dictionaryWithObject:responseData forKey:kLocalyticsKeyResponseBody];
if (target) {
[target performSelector:callbackMethod withObject:userInfo];
}
}
} }
@catch (NSException * e) {} @catch (NSException * e) {}
@ -195,6 +231,15 @@ static LocalyticsUploader *_sharedUploader = nil;
} }
} }
/*!
@method uploadTimeStamp
@abstract Gets the current time, along with local timezone, formatted as a DateTime for the webservice.
@return a DateTime of the current local time and timezone.
*/
- (NSString *)uploadTimeStamp {
return [ NSString stringWithFormat:@"%ld", (long)[[NSDate date] timeIntervalSince1970] ];
}
#pragma mark - System Functions #pragma mark - System Functions
+ (id)allocWithZone:(NSZone *)zone { + (id)allocWithZone:(NSZone *)zone {
@synchronized(self) { @synchronized(self) {

View File

@ -1,48 +0,0 @@
// UploaderThread.h
// Copyright (C) 2009 Char Software Inc., DBA Localytics
//
// This code is provided under the Localytics Modified BSD License.
// A copy of this license has been distributed in a file called LICENSE
// with this source code.
//
// Please visit www.localytics.com for more information.
#import <UIKit/UIKit.h>
/*!
@class UploaderThread
@discussion Singleton class to handle data uploads
*/
@interface UploaderThread : NSObject {
NSURLConnection *_uploadConnection; // The connection which uploads the bits
NSInteger _responseStatusCode; // The HTTP response status code for the current connection
BOOL _isUploading; // A flag to gaurantee only one uploader instance can happen at once
}
@property (nonatomic, retain) NSURLConnection *uploadConnection;
@property BOOL isUploading;
/*!
@method sharedUploaderThread
@abstract Establishes this as a Singleton Class allowing for data persistence.
The class is accessed within the code using the following syntax:
[[UploaderThread sharedUploaderThread] functionHere]
*/
+ (UploaderThread *)sharedUploaderThread;
/*!
@method UploaderThread
@abstract Creates a thread which uploads all queued header and event data.
All files starting with sessionFilePrefix are renamed,
uploaded and deleted on upload. This way the sessions can continue
writing data regardless of whether or not the upload succeeds. Files
which have been renamed still count towards the total number of Localytics
files which can be stored on the disk.
@param localyticsApplicationKey the Localytics application ID
*/
- (void)uploaderThreadwithApplicationKey:(NSString *)localyticsApplicationKey;
@end

View File

@ -1,260 +0,0 @@
// UploaderThread.m
// Copyright (C) 2009 Char Software Inc., DBA Localytics
//
// This code is provided under the Localytics Modified BSD License.
// A copy of this license has been distributed in a file called LICENSE
// with this source code.
//
// Please visit www.localytics.com for more information.
#import "UploaderThread.h"
#import "LocalyticsSession.h"
#import "LocalyticsDatabase.h"
#import <zlib.h>
#define LOCALYTICS_URL @"http://analytics.localytics.com/api/v2/applications/%@/uploads" // url to send the
static UploaderThread *_sharedUploaderThread = nil;
@interface UploaderThread ()
- (void)complete;
- (NSData *)gzipDeflatedDataWithData:(NSData *)data;
- (void)logMessage:(NSString *)message;
@end
@implementation UploaderThread
@synthesize uploadConnection = _uploadConnection;
@synthesize isUploading = _isUploading;
#pragma mark Singleton Class
+ (UploaderThread *)sharedUploaderThread {
@synchronized(self) {
if (_sharedUploaderThread == nil)
{
_sharedUploaderThread = [[self alloc] init];
}
}
return _sharedUploaderThread;
}
#pragma mark Class Methods
- (void)uploaderThreadwithApplicationKey:(NSString *)localyticsApplicationKey {
// Do nothing if already uploading.
if (self.uploadConnection != nil || self.isUploading == true)
{
[self logMessage:@"Upload already in progress. Aborting."];
return;
}
[self logMessage:@"Beginning upload process"];
self.isUploading = true;
// Prepare the data for upload. The upload could take a long time, so some effort has to be made to be sure that events
// which get written while the upload is taking place don't get lost or duplicated. To achieve this, the logic is:
// 1) Append every header row blob string and and those of its associated events to the upload string.
// 2) Deflate and upload the data.
// 3) On success, delete all blob headers and staged events. Events added while an upload is in process are not
// deleted because they are not associated a header (and cannot be until the upload completes).
// Step 1
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
LocalyticsDatabase *db = [LocalyticsDatabase sharedLocalyticsDatabase];
NSString *blobString = [db uploadBlobString];
if ([blobString length] == 0) {
// There is nothing outstanding to upload.
[self logMessage:@"Abandoning upload. There are no new events."];
[pool drain];
[self complete];
return;
}
NSData *requestData = [blobString dataUsingEncoding:NSUTF8StringEncoding];
NSString *myString = [[[NSString alloc] initWithData:requestData encoding:NSUTF8StringEncoding] autorelease];
[self logMessage:@"Upload data:"];
[self logMessage:myString];
// Step 2
NSData *deflatedRequestData = [[self gzipDeflatedDataWithData:requestData] retain];
[pool drain];
NSString *apiUrlString = [NSString stringWithFormat:LOCALYTICS_URL, [localyticsApplicationKey stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]];
NSMutableURLRequest *submitRequest = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:apiUrlString]
cachePolicy:NSURLRequestReloadIgnoringCacheData
timeoutInterval:60.0];
[submitRequest setHTTPMethod:@"POST"];
[submitRequest setValue:@"application/x-gzip" forHTTPHeaderField:@"Content-Type"];
[submitRequest setValue:@"gzip" forHTTPHeaderField:@"Content-Encoding"];
[submitRequest setValue:[NSString stringWithFormat:@"%d", [deflatedRequestData length]] forHTTPHeaderField:@"Content-Length"];
[submitRequest setHTTPBody:deflatedRequestData];
[deflatedRequestData release];
// The NSURLConnection Object automatically spawns its own thread as a default behavior.
@try
{
[self logMessage:@"Spawning new thread for upload"];
self.uploadConnection = [NSURLConnection connectionWithRequest:submitRequest delegate:self];
// Step 3 is handled by connectionDidFinishLoading.
}
@catch (NSException * e)
{
[self complete];
}
}
#pragma mark **** NSURLConnection FUNCTIONS ****
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
// Used to gather response data from server - Not utilized in this version
}
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response {
// Could receive multiple response callbacks, likely due to redirection.
// Record status and act only when connection completes load.
_responseStatusCode = [(NSHTTPURLResponse *)response statusCode];
}
- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
// If the connection finished loading, the files should be deleted. While response status codes in the 5xx range
// leave upload rows intact, the default case is to delete.
if (_responseStatusCode >= 500 && _responseStatusCode < 600)
{
[self logMessage:[NSString stringWithFormat:@"Upload failed with response status code %d", _responseStatusCode]];
} else
{
// The connection finished loading and uploaded data should be deleted. Because only one instance of the
// uploader can be running at a time it should not be possible for new upload rows to appear so there is no
// fear of deleting data which has not yet been uploaded.
[self logMessage:[NSString stringWithFormat:@"Upload completed successfully. Response code %d", _responseStatusCode]];
[[LocalyticsDatabase sharedLocalyticsDatabase] deleteUploadData];
}
// Close upload session
[self complete];
}
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error {
// On error, simply print the error and close the uploader. We have to assume the data was not transmited
// so it is not deleted. In the event that we accidently store data which was succesfully uploaded, the
// duplicate data will be ignored by the server when it is next uploaded.
[self logMessage:[NSString stringWithFormat:
@"Error Uploading. Code: %d, Description: %s",
[error code],
[error localizedDescription]]];
[self complete];
}
/*!
@method complete
@abstract closes the upload connection and reports back to the session that the upload is complete
*/
- (void)complete {
_responseStatusCode = 0;
self.uploadConnection = nil;
self.isUploading = false;
}
/*!
@method gzipDeflatedDataWithData
@abstract Deflates the provided data using gzip at the default compression level (6). Complete NSData gzip category available on CocoaDev. http://www.cocoadev.com/index.pl?NSDataCategory.
@return the deflated data
*/
- (NSData *)gzipDeflatedDataWithData:(NSData *)data
{
if ([data length] == 0) return data;
z_stream strm;
strm.zalloc = Z_NULL;
strm.zfree = Z_NULL;
strm.opaque = Z_NULL;
strm.total_out = 0;
strm.next_in=(Bytef *)[data bytes];
strm.avail_in = [data length];
// Compresssion Levels:
// Z_NO_COMPRESSION
// Z_BEST_SPEED
// Z_BEST_COMPRESSION
// Z_DEFAULT_COMPRESSION
if (deflateInit2(&strm, Z_DEFAULT_COMPRESSION, Z_DEFLATED, (15+16), 8, Z_DEFAULT_STRATEGY) != Z_OK) return nil;
NSMutableData *compressed = [NSMutableData dataWithLength:16384]; // 16K chunks for expansion
do {
if (strm.total_out >= [compressed length])
[compressed increaseLengthBy: 16384];
strm.next_out = [compressed mutableBytes] + strm.total_out;
strm.avail_out = [compressed length] - strm.total_out;
deflate(&strm, Z_FINISH);
} while (strm.avail_out == 0);
deflateEnd(&strm);
[compressed setLength: strm.total_out];
return [NSData dataWithData:compressed];
}
/*!
@method logMessage
@abstract Logs a message with (localytics uploader) prepended to it
@param message The message to log
*/
- (void) logMessage:(NSString *)message {
if(DO_LOCALYTICS_LOGGING) {
NSLog(@"(localytics uploader) %s\n", [message UTF8String]);
}
}
#pragma mark System Functions
+ (id)allocWithZone:(NSZone *)zone {
@synchronized(self) {
if (_sharedUploaderThread == nil) {
_sharedUploaderThread = [super allocWithZone:zone];
return _sharedUploaderThread;
}
}
// returns nil on subsequent allocations
return nil;
}
- (id)copyWithZone:(NSZone *)zone {
return self;
}
- (id)retain {
return self;
}
- (unsigned)retainCount {
// maximum value of an unsigned int - prevents additional retains for the class
return UINT_MAX;
}
- (oneway void)release {
// ignore release commands
}
- (id)autorelease {
return self;
}
- (void)dealloc {
[_uploadConnection release];
[_sharedUploaderThread release];
[super dealloc];
}
@end

View File

@ -1,5 +1,5 @@
// WebserviceConstants.h // WebserviceConstants.h
// Copyright (C) 2009 Char Software Inc., DBA Localytics // Copyright (C) 2012 Char Software Inc., DBA Localytics
// //
// This code is provided under the Localytics Modified BSD License. // This code is provided under the Localytics Modified BSD License.
// A copy of this license has been distributed in a file called LICENSE // A copy of this license has been distributed in a file called LICENSE
@ -11,6 +11,12 @@
// To save disk space and network bandwidth all the keywords have been // To save disk space and network bandwidth all the keywords have been
// abbreviated and are exploded by the server. // abbreviated and are exploded by the server.
/*****************
* Upload Header *
*****************/
#define HEADER_CLIENT_TIME @"x-upload-time"
#define HEADER_INSTALL_ID @"x-install-id"
/********************* /*********************
* Shared Attributes * * Shared Attributes *
*********************/ *********************/
@ -22,6 +28,7 @@
#define PARAM_SESSION_UUID @"su" // UUID for an existing session #define PARAM_SESSION_UUID @"su" // UUID for an existing session
#define PARAM_NEW_SESSION_UUID @"u" // UUID for a new session #define PARAM_NEW_SESSION_UUID @"u" // UUID for a new session
#define PARAM_ATTRIBUTES @"attrs" // Attributes (dictionary) #define PARAM_ATTRIBUTES @"attrs" // Attributes (dictionary)
#define PARAM_SESSION_ELAPSE_TIME @"sl" // Number of seconds since the previous session start
/*************** /***************
* Blob Header * * Blob Header *
@ -42,9 +49,8 @@
// PARAM_DATA_TYPE // PARAM_DATA_TYPE
#define PARAM_APP_KEY @"au" // Localytics Application ID #define PARAM_APP_KEY @"au" // Localytics Application ID
#define PARAM_DEVICE_UUID @"du" // Device UUID
#define PARAM_DEVICE_UUID_HASHED @"udid" // Hashed version of the UUID #define PARAM_DEVICE_UUID_HASHED @"udid" // Hashed version of the UUID
#define PARAM_DEVICE_MAC @"wmac" // Hashed version of the device Mac #define PARAM_DEVICE_ADID @"adid" // Advertising Identifier
#define PARAM_INSTALL_ID @"iu" // Install ID #define PARAM_INSTALL_ID @"iu" // Install ID
#define PARAM_JAILBROKEN @"j" // Jailbroken (boolean) #define PARAM_JAILBROKEN @"j" // Jailbroken (boolean)
#define PARAM_LIBRARY_VERSION @"lv" // Client Version #define PARAM_LIBRARY_VERSION @"lv" // Client Version
@ -52,14 +58,11 @@
#define PARAM_DEVICE_PLATFORM @"dp" // Device Platform #define PARAM_DEVICE_PLATFORM @"dp" // Device Platform
#define PARAM_LOCALE_LANGUAGE @"dll" // Locale Language #define PARAM_LOCALE_LANGUAGE @"dll" // Locale Language
#define PARAM_LOCALE_COUNTRY @"dlc" // Locale Country #define PARAM_LOCALE_COUNTRY @"dlc" // Locale Country
#define PARAM_NETWORK_COUNTRY @"nc" // Network Country (iso code) // ???: Never used on iPhone.
#define PARAM_DEVICE_COUNTRY @"dc" // Device Country (iso code) #define PARAM_DEVICE_COUNTRY @"dc" // Device Country (iso code)
#define PARAM_DEVICE_MANUFACTURER @"dma" // Device Manufacturer // ???: Never used on iPhone. Used to be "Device Make".
#define PARAM_DEVICE_MODEL @"dmo" // Device Model #define PARAM_DEVICE_MODEL @"dmo" // Device Model
#define PARAM_DEVICE_OS_VERSION @"dov" // Device OS Version #define PARAM_DEVICE_OS_VERSION @"dov" // Device OS Version
#define PARAM_NETWORK_CARRIER @"nca" // Network Carrier #define PARAM_NETWORK_CARRIER @"nca" // Network Carrier
#define PARAM_DATA_CONNECTION @"dac" // Data Connection Type // ???: Never used on iPhone. #define PARAM_OPT_VALUE @"out" // Opt Out (boolean)
#define PARAM_OPT_VALUE @"optin" // Opt In (boolean)
#define PARAM_DEVICE_MEMORY @"dmem" // Device Memory #define PARAM_DEVICE_MEMORY @"dmem" // Device Memory
/***************** /*****************

View File

@ -46,11 +46,7 @@
}]; }];
} }
if (![managedObjectContext.persistentStoreCoordinator.persistentStores count]) [[self storeManager] persistentStoreCoordinator];
[managedObjectContext performBlockAndWait:^{
managedObjectContext.persistentStoreCoordinator = [self storeManager].persistentStoreCoordinator;
}];
if (![self storeManager].isReady) if (![self storeManager].isReady)
return nil; return nil;

View File

@ -116,6 +116,7 @@
NSString *localyticsKey = [self localyticsKey]; NSString *localyticsKey = [self localyticsKey];
if ([localyticsKey length]) { if ([localyticsKey length]) {
inf(@"Initializing Localytics"); inf(@"Initializing Localytics");
[LocalyticsSession sharedLocalyticsSession].enableHTTPS = YES;
[[LocalyticsSession sharedLocalyticsSession] startSession:localyticsKey]; [[LocalyticsSession sharedLocalyticsSession] startSession:localyticsKey];
[[PearlLogger get] registerListener:^BOOL(PearlLogMessage *message) { [[PearlLogger get] registerListener:^BOOL(PearlLogMessage *message) {
if (message.level >= PearlLogLevelWarn) if (message.level >= PearlLogLevelWarn)
@ -321,23 +322,11 @@
- (void)applicationDidReceiveMemoryWarning:(UIApplication *)application { - (void)applicationDidReceiveMemoryWarning:(UIApplication *)application {
wrn(@"Received memory warning."); inf(@"Received memory warning.");
[super applicationDidReceiveMemoryWarning:application]; [super applicationDidReceiveMemoryWarning:application];
} }
- (void)applicationDidBecomeActive:(UIApplication *)application {
inf(@"Re-activated");
[[MPAppDelegate get] checkConfig];
if (FBSession.activeSession.state == FBSessionStateCreatedOpening)
// An old Facebook Login session that wasn't finished. Clean it up.
[FBSession.activeSession close];
[super applicationDidBecomeActive:application];
}
- (void)applicationDidEnterBackground:(UIApplication *)application { - (void)applicationDidEnterBackground:(UIApplication *)application {
[[LocalyticsSession sharedLocalyticsSession] close]; [[LocalyticsSession sharedLocalyticsSession] close];
@ -369,10 +358,31 @@
- (void)applicationWillResignActive:(UIApplication *)application { - (void)applicationWillResignActive:(UIApplication *)application {
inf(@"Will deactivate"); inf(@"Will deactivate");
[self saveContext]; [self saveContext];
if (![[MPiOSConfig get].rememberLogin boolValue]) if (![[MPiOSConfig get].rememberLogin boolValue])
[self signOutAnimated:NO]; [self signOutAnimated:NO];
[[LocalyticsSession sharedLocalyticsSession] close];
[[LocalyticsSession sharedLocalyticsSession] upload];
[super applicationWillResignActive:application];
}
- (void)applicationDidBecomeActive:(UIApplication *)application {
inf(@"Re-activated");
[[MPAppDelegate get] checkConfig];
if (FBSession.activeSession.state == FBSessionStateCreatedOpening)
// An old Facebook Login session that wasn't finished. Clean it up.
[FBSession.activeSession close];
[[LocalyticsSession sharedLocalyticsSession] resume];
[[LocalyticsSession sharedLocalyticsSession] upload];
[super applicationDidBecomeActive:application];
} }
#pragma mark - Behavior #pragma mark - Behavior

View File

@ -77,6 +77,13 @@
[super viewWillAppear:animated]; [super viewWillAppear:animated];
} }
- (void)viewDidAppear:(BOOL)animated {
[[LocalyticsSession sharedLocalyticsSession] tagScreen:@"Apps"];
[super viewDidAppear:animated];
}
- (void)viewWillDisappear:(BOOL)animated { - (void)viewWillDisappear:(BOOL)animated {
[[UIApplication sharedApplication] setStatusBarHidden:NO withAnimation:UIStatusBarAnimationSlide]; [[UIApplication sharedApplication] setStatusBarHidden:NO withAnimation:UIStatusBarAnimationSlide];

View File

@ -7,6 +7,7 @@
// //
#import "MPGuideViewController.h" #import "MPGuideViewController.h"
#import "LocalyticsSession.h"
@implementation MPGuideViewController @implementation MPGuideViewController
@ -35,6 +36,8 @@
- (void)viewDidAppear:(BOOL)animated { - (void)viewDidAppear:(BOOL)animated {
[[LocalyticsSession sharedLocalyticsSession] tagScreen:@"Guide"];
[super viewDidAppear:animated]; [super viewDidAppear:animated];
} }

View File

@ -149,7 +149,7 @@
inf(@"Main will appear"); inf(@"Main will appear");
// Sometimes, the search bar gets stuck in some sort of first-responder mode that it can't get out of... // Sometimes, the search bar gets stuck in some sort of first-responder mode that it can't get out of...
[self.searchDisplayController.searchBar resignFirstResponder]; [[self.view.window findFirstResponderInHierarchy] resignFirstResponder];
// Needed for when we appear after a modal VC dismisses: // Needed for when we appear after a modal VC dismisses:
// We can't present until the other modal VC has been fully dismissed and presenting in viewDidAppear will fail. // We can't present until the other modal VC has been fully dismissed and presenting in viewDidAppear will fail.
@ -204,6 +204,8 @@
}]; }];
[[LocalyticsSession sharedLocalyticsSession] tagScreen:@"Main"];
[super viewDidAppear:animated]; [super viewDidAppear:animated];
} }
@ -676,7 +678,7 @@
- (IBAction)action:(id)sender { - (IBAction)action:(id)sender {
[PearlSheet showSheetWithTitle:nil message:nil viewStyle:UIActionSheetStyleAutomatic [PearlSheet showSheetWithTitle:nil viewStyle:UIActionSheetStyleAutomatic
initSheet:nil initSheet:nil
tappedButtonBlock:^(UIActionSheet *sheet, NSInteger buttonIndex) { tappedButtonBlock:^(UIActionSheet *sheet, NSInteger buttonIndex) {
if (buttonIndex == [sheet cancelButtonIndex]) if (buttonIndex == [sheet cancelButtonIndex])

View File

@ -11,6 +11,7 @@
#import "MPAppDelegate.h" #import "MPAppDelegate.h"
#import "MPAppDelegate_Key.h" #import "MPAppDelegate_Key.h"
#import "MPAppDelegate_Store.h" #import "MPAppDelegate_Store.h"
#import "LocalyticsSession.h"
@interface MPPreferencesViewController () @interface MPPreferencesViewController ()
@ -80,6 +81,13 @@
[super viewWillAppear:animated]; [super viewWillAppear:animated];
} }
- (void)viewDidAppear:(BOOL)animated {
[[LocalyticsSession sharedLocalyticsSession] tagScreen:@"Preferences"];
[super viewDidAppear:animated];
}
- (void)viewWillDisappear:(BOOL)animated { - (void)viewWillDisappear:(BOOL)animated {
inf(@"Preferences will disappear"); inf(@"Preferences will disappear");
@ -152,6 +160,7 @@
vc.showDoneButton = NO; vc.showDoneButton = NO;
[self.navigationController pushViewController:vc animated:YES]; [self.navigationController pushViewController:vc animated:YES];
[[LocalyticsSession sharedLocalyticsSession] tagScreen:@"Settings"];
} }
@end @end

View File

@ -7,6 +7,7 @@
// //
#import "MPTypeViewController.h" #import "MPTypeViewController.h"
#import "LocalyticsSession.h"
@interface MPTypeViewController () @interface MPTypeViewController ()
@ -44,6 +45,8 @@
} }
}]; }];
[[LocalyticsSession sharedLocalyticsSession] tagScreen:@"Type Selection"];
[super viewDidAppear:animated]; [super viewDidAppear:animated];
} }

View File

@ -16,6 +16,7 @@
#import "MPAppDelegate.h" #import "MPAppDelegate.h"
#import "MPAppDelegate_Key.h" #import "MPAppDelegate_Key.h"
#import "MPAppDelegate_Store.h" #import "MPAppDelegate_Store.h"
#import "LocalyticsSession.h"
@interface MPUnlockViewController () @interface MPUnlockViewController ()
@ -193,6 +194,8 @@
self.uiContainer.alpha = 1; self.uiContainer.alpha = 1;
}]; }];
[[LocalyticsSession sharedLocalyticsSession] tagScreen:@"Unlock"];
[super viewDidAppear:animated]; [super viewDidAppear:animated];
} }
@ -749,7 +752,7 @@
return; return;
[PearlSheet showSheetWithTitle:targetedUser.name [PearlSheet showSheetWithTitle:targetedUser.name
message:nil viewStyle:UIActionSheetStyleBlackTranslucent viewStyle:UIActionSheetStyleBlackTranslucent
initSheet:nil tappedButtonBlock:^(UIActionSheet *sheet, NSInteger buttonIndex) { initSheet:nil tappedButtonBlock:^(UIActionSheet *sheet, NSInteger buttonIndex) {
if (buttonIndex == [sheet cancelButtonIndex]) if (buttonIndex == [sheet cancelButtonIndex])
return; return;
@ -823,7 +826,7 @@
- (IBAction)add:(UIButton *)sender { - (IBAction)add:(UIButton *)sender {
[PearlSheet showSheetWithTitle:@"Follow Master Password" message:nil viewStyle:UIActionSheetStyleBlackTranslucent [PearlSheet showSheetWithTitle:@"Follow Master Password" viewStyle:UIActionSheetStyleBlackTranslucent
initSheet:nil tappedButtonBlock:^(UIActionSheet *sheet, NSInteger buttonIndex) { initSheet:nil tappedButtonBlock:^(UIActionSheet *sheet, NSInteger buttonIndex) {
if (buttonIndex == [sheet cancelButtonIndex]) if (buttonIndex == [sheet cancelButtonIndex])
return; return;