Crashlytics, Localytics, TestFlight + password change warnings.
[ADDED] Crashlytics, Localytics. [IMPROVED] Async TestFlight takeOff. [REMOVED] TestFlight token hidden. [FIXED] Warnings, mostly to do with sign conversions. [ADDED] Warning messages whenever site's password changes, allowing the user to cancel the operation. [ADDED] Make password counter resettable by holding down on the counter increment button.
This commit is contained in:
10
Localytics/Localytics.plist
Normal file
10
Localytics/Localytics.plist
Normal file
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>Key.development</key>
|
||||
<string>e6238ceba8ec92832e77b1b-9ccd60bc-c39b-11e0-06e4-007f58cb3154</string>
|
||||
<key>Key.distribution</key>
|
||||
<string></string>
|
||||
</dict>
|
||||
</plist>
|
57
Localytics/LocalyticsDatabase.h
Normal file
57
Localytics/LocalyticsDatabase.h
Normal file
@@ -0,0 +1,57 @@
|
||||
//
|
||||
// LocalyticsDatabase.h
|
||||
// LocalyticsDemo
|
||||
//
|
||||
// Created by jkaufman on 5/26/11.
|
||||
// Copyright 2011 Localytics. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <sqlite3.h>
|
||||
|
||||
#define MAX_DATABASE_SIZE 500000 // The maximum allowed disk size of the primary database file at open, in bytes
|
||||
#define VACUUM_THRESHOLD 0.8 // The database is vacuumed after its size exceeds this proportion of the maximum.
|
||||
|
||||
@interface LocalyticsDatabase : NSObject {
|
||||
sqlite3 *_databaseConnection;
|
||||
}
|
||||
|
||||
+ (LocalyticsDatabase *)sharedLocalyticsDatabase;
|
||||
|
||||
- (NSUInteger)databaseSize;
|
||||
- (int)eventCount;
|
||||
- (NSTimeInterval)createdTimestamp;
|
||||
|
||||
- (BOOL)beginTransaction:(NSString *)name;
|
||||
- (BOOL)releaseTransaction:(NSString *)name;
|
||||
- (BOOL)rollbackTransaction:(NSString *)name;
|
||||
|
||||
- (BOOL)incrementLastUploadNumber:(int *)uploadNumber;
|
||||
- (BOOL)incrementLastSessionNumber:(int *)sessionNumber;
|
||||
|
||||
- (BOOL)addEventWithBlobString:(NSString *)blob;
|
||||
- (BOOL)addCloseEventWithBlobString:(NSString *)blob;
|
||||
- (BOOL)addFlowEventWithBlobString:(NSString *)blob;
|
||||
- (BOOL)removeLastCloseAndFlowEvents;
|
||||
|
||||
- (BOOL)addHeaderWithSequenceNumber:(int)number blobString:(NSString *)blob rowId:(sqlite3_int64 *)insertedRowId;
|
||||
- (int)unstagedEventCount;
|
||||
- (BOOL)stageEventsForUpload:(sqlite3_int64)headerId;
|
||||
- (BOOL)updateAppKey:(NSString *)appKey;
|
||||
- (NSString *)uploadBlobString;
|
||||
- (BOOL)deleteUploadedData;
|
||||
- (BOOL)resetAnalyticsData;
|
||||
- (BOOL)vacuumIfRequired;
|
||||
|
||||
- (NSTimeInterval)lastSessionStartTimestamp;
|
||||
- (BOOL)setLastsessionStartTimestamp:(NSTimeInterval)timestamp;
|
||||
|
||||
- (BOOL)isOptedOut;
|
||||
- (BOOL)setOptedOut:(BOOL)optOut;
|
||||
- (NSString *)installId;
|
||||
- (NSString *)appKey; // Most recent app key-- may not be that used to open the session.
|
||||
|
||||
- (NSString *)customDimension:(int)dimension;
|
||||
- (BOOL)setCustomDimension:(int)dimension value:(NSString *)value;
|
||||
|
||||
@end
|
743
Localytics/LocalyticsDatabase.m
Normal file
743
Localytics/LocalyticsDatabase.m
Normal file
@@ -0,0 +1,743 @@
|
||||
//
|
||||
// LocalyticsDatabase.m
|
||||
// LocalyticsDemo
|
||||
//
|
||||
// Created by jkaufman on 5/26/11.
|
||||
// Copyright 2011 Localytics. All rights reserved.
|
||||
//
|
||||
|
||||
#import "LocalyticsDatabase.h"
|
||||
|
||||
#define LOCALYTICS_DIR @".localytics" // Name for the directory in which Localytics database is stored
|
||||
#define LOCALYTICS_DB @"localytics" // File name for the database (without extension)
|
||||
#define BUSY_TIMEOUT 30 // Maximum time SQlite will busy-wait for the database to unlock before returning SQLITE_BUSY
|
||||
|
||||
@interface LocalyticsDatabase ()
|
||||
- (int)schemaVersion;
|
||||
- (void)createSchema;
|
||||
- (void)upgradeToSchemaV2;
|
||||
- (void)upgradeToSchemaV3;
|
||||
- (void)moveDbToCaches;
|
||||
- (NSString *)randomUUID;
|
||||
@end
|
||||
|
||||
@implementation LocalyticsDatabase
|
||||
|
||||
// The singleton database object.
|
||||
static LocalyticsDatabase *_sharedLocalyticsDatabase = nil;
|
||||
|
||||
+ (NSString *)localyticsDirectoryPath {
|
||||
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);
|
||||
return [[paths objectAtIndex:0] stringByAppendingPathComponent:LOCALYTICS_DIR];
|
||||
}
|
||||
|
||||
+ (NSString *)localyticsDatabasePath {
|
||||
NSString *path = [[LocalyticsDatabase localyticsDirectoryPath] stringByAppendingPathComponent:[NSString stringWithFormat:@"%@.sqlite", LOCALYTICS_DB]];
|
||||
return path;
|
||||
}
|
||||
|
||||
#pragma mark Singleton Class
|
||||
+ (LocalyticsDatabase *)sharedLocalyticsDatabase {
|
||||
@synchronized(self) {
|
||||
if (_sharedLocalyticsDatabase == nil) {
|
||||
_sharedLocalyticsDatabase = [[self alloc] init];
|
||||
}
|
||||
}
|
||||
return _sharedLocalyticsDatabase;
|
||||
}
|
||||
|
||||
- (LocalyticsDatabase *)init {
|
||||
if((self = [super init])) {
|
||||
|
||||
// Mover any data that a previous library may have left in the documents directory
|
||||
[self moveDbToCaches];
|
||||
|
||||
// Create directory structure for Localytics.
|
||||
NSString *directoryPath = [LocalyticsDatabase localyticsDirectoryPath];
|
||||
if (![[NSFileManager defaultManager] fileExistsAtPath:directoryPath]) {
|
||||
[[NSFileManager defaultManager] createDirectoryAtPath:directoryPath withIntermediateDirectories:YES attributes:nil error:nil];
|
||||
}
|
||||
|
||||
// Attempt to open database. It will be created if it does not exist, already.
|
||||
NSString *dbPath = [LocalyticsDatabase localyticsDatabasePath];
|
||||
int code = sqlite3_open([dbPath UTF8String], &_databaseConnection);
|
||||
|
||||
// If we were unable to open the database, it is likely corrupted. Clobber it and move on.
|
||||
if (code != SQLITE_OK) {
|
||||
[[NSFileManager defaultManager] removeItemAtPath:dbPath error:nil];
|
||||
code = sqlite3_open([dbPath UTF8String], &_databaseConnection);
|
||||
}
|
||||
|
||||
// Check db connection, creating schema if necessary.
|
||||
if (code == SQLITE_OK) {
|
||||
sqlite3_busy_timeout(_databaseConnection, BUSY_TIMEOUT); // Defaults to 0, otherwise.
|
||||
if ([self schemaVersion] == 0) {
|
||||
[self createSchema];
|
||||
}
|
||||
}
|
||||
|
||||
// Perform any Migrations if necessary
|
||||
if ([self schemaVersion] < 2) {
|
||||
[self upgradeToSchemaV2];
|
||||
}
|
||||
if ([self schemaVersion] < 3) {
|
||||
[self upgradeToSchemaV3];
|
||||
}
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
#pragma mark - Database
|
||||
|
||||
- (BOOL)beginTransaction:(NSString *)name {
|
||||
const char *sql = [[NSString stringWithFormat:@"SAVEPOINT %@", name] cStringUsingEncoding:NSUTF8StringEncoding];
|
||||
int code = sqlite3_exec(_databaseConnection, sql, NULL, NULL, NULL);
|
||||
return code == SQLITE_OK;
|
||||
}
|
||||
|
||||
- (BOOL)releaseTransaction:(NSString *)name {
|
||||
const char *sql = [[NSString stringWithFormat:@"RELEASE SAVEPOINT %@", name] cStringUsingEncoding:NSUTF8StringEncoding];
|
||||
int code = sqlite3_exec(_databaseConnection, sql, NULL, NULL, NULL);
|
||||
return code == SQLITE_OK;
|
||||
}
|
||||
|
||||
- (BOOL)rollbackTransaction:(NSString *)name {
|
||||
const char *sql = [[NSString stringWithFormat:@"ROLLBACK SAVEPOINT %@", name] cStringUsingEncoding:NSUTF8StringEncoding];
|
||||
int code = sqlite3_exec(_databaseConnection, sql, NULL, NULL, NULL);
|
||||
return code == SQLITE_OK;
|
||||
}
|
||||
|
||||
- (int)schemaVersion {
|
||||
int version = 0;
|
||||
const char *sql = "SELECT MAX(schema_version) FROM localytics_info";
|
||||
sqlite3_stmt *selectSchemaVersion;
|
||||
if(sqlite3_prepare_v2(_databaseConnection, sql, -1, &selectSchemaVersion, NULL) == SQLITE_OK) {
|
||||
if(sqlite3_step(selectSchemaVersion) == SQLITE_ROW) {
|
||||
version = sqlite3_column_int(selectSchemaVersion, 0);
|
||||
}
|
||||
}
|
||||
sqlite3_finalize(selectSchemaVersion);
|
||||
return version;
|
||||
}
|
||||
|
||||
- (NSString *)installId {
|
||||
NSString *installId = nil;
|
||||
|
||||
sqlite3_stmt *selectInstallId;
|
||||
sqlite3_prepare_v2(_databaseConnection, "SELECT install_id FROM localytics_info", -1, &selectInstallId, NULL);
|
||||
int code = sqlite3_step(selectInstallId);
|
||||
if (code == SQLITE_ROW && sqlite3_column_text(selectInstallId, 0)) {
|
||||
installId = [NSString stringWithUTF8String:(char *)sqlite3_column_text(selectInstallId, 0)];
|
||||
}
|
||||
sqlite3_finalize(selectInstallId);
|
||||
|
||||
return installId;
|
||||
}
|
||||
|
||||
- (NSString *)appKey {
|
||||
NSString *appKey = nil;
|
||||
|
||||
sqlite3_stmt *selectAppKey;
|
||||
sqlite3_prepare_v2(_databaseConnection, "SELECT app_key FROM localytics_info", -1, &selectAppKey, NULL);
|
||||
int code = sqlite3_step(selectAppKey);
|
||||
if (code == SQLITE_ROW && sqlite3_column_text(selectAppKey, 0)) {
|
||||
appKey = [NSString stringWithUTF8String:(char *)sqlite3_column_text(selectAppKey, 0)];
|
||||
}
|
||||
sqlite3_finalize(selectAppKey);
|
||||
|
||||
return appKey;
|
||||
}
|
||||
|
||||
// Due to the new iOS storage guidelines it is necessary to move the database out of the documents directory
|
||||
// and into the /library/caches directory
|
||||
- (void)moveDbToCaches {
|
||||
NSArray *documentPaths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
|
||||
NSString *localyticsDocumentsDirectory = [[documentPaths objectAtIndex:0] stringByAppendingPathComponent:LOCALYTICS_DIR];
|
||||
NSArray *cachesPaths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);
|
||||
NSString *localyticsCachesDirectory = [[cachesPaths objectAtIndex:0] stringByAppendingPathComponent:LOCALYTICS_DIR];
|
||||
|
||||
// If the old directory doesn't exist, there is nothing else to do here
|
||||
if([[NSFileManager defaultManager] fileExistsAtPath:localyticsDocumentsDirectory] == NO)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Try to move the directory
|
||||
if(NO == [[NSFileManager defaultManager] moveItemAtPath:localyticsDocumentsDirectory
|
||||
toPath:localyticsCachesDirectory
|
||||
error:nil])
|
||||
{
|
||||
// If the move failed try and, delete the old directory
|
||||
[ [NSFileManager defaultManager] removeItemAtPath:localyticsDocumentsDirectory error:nil];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)createSchema {
|
||||
int code = SQLITE_OK;
|
||||
|
||||
// Execute schema creation within a single transaction.
|
||||
code = sqlite3_exec(_databaseConnection, "BEGIN", NULL, NULL, NULL);
|
||||
|
||||
if (code == SQLITE_OK) {
|
||||
code = sqlite3_exec(_databaseConnection,
|
||||
"CREATE TABLE upload_headers ("
|
||||
"sequence_number INTEGER PRIMARY KEY, "
|
||||
"blob_string TEXT)",
|
||||
NULL, NULL, NULL);
|
||||
}
|
||||
|
||||
if (code == SQLITE_OK) {
|
||||
code = sqlite3_exec(_databaseConnection,
|
||||
"CREATE TABLE events ("
|
||||
"event_id INTEGER PRIMARY KEY AUTOINCREMENT, " // In case foreign key constraints are reintroduced.
|
||||
"upload_header INTEGER, "
|
||||
"blob_string TEXT NOT NULL)",
|
||||
NULL, NULL, NULL);
|
||||
}
|
||||
|
||||
if (code == SQLITE_OK) {
|
||||
code = sqlite3_exec(_databaseConnection,
|
||||
"CREATE TABLE localytics_info ("
|
||||
"schema_version INTEGER PRIMARY KEY, "
|
||||
"last_upload_number INTEGER, "
|
||||
"last_session_number INTEGER, "
|
||||
"opt_out BOOLEAN, "
|
||||
"last_close_event INTEGER, "
|
||||
"last_flow_event INTEGER, "
|
||||
"last_session_start REAL, "
|
||||
"app_key CHAR(64), "
|
||||
"custom_d0 CHAR(64), "
|
||||
"custom_d1 CHAR(64), "
|
||||
"custom_d2 CHAR(64), "
|
||||
"custom_d3 CHAR(64) "
|
||||
")",
|
||||
NULL, NULL, NULL);
|
||||
}
|
||||
|
||||
if (code == SQLITE_OK) {
|
||||
code = sqlite3_exec(_databaseConnection,
|
||||
"INSERT INTO localytics_info (schema_version, last_upload_number, last_session_number, opt_out) "
|
||||
"VALUES (3, 0, 0, 0)", 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);
|
||||
}
|
||||
}
|
||||
|
||||
// V2 adds a unique identifier for each installation
|
||||
// This identifier has been moved to user preferences so the database an live in the caches directory
|
||||
// Also adds storage for custom dimensions
|
||||
- (void)upgradeToSchemaV2 {
|
||||
int code = SQLITE_OK;
|
||||
|
||||
code = sqlite3_exec(_databaseConnection, "BEGIN", NULL, NULL, NULL);
|
||||
|
||||
if (code == SQLITE_OK) {
|
||||
code = sqlite3_exec(_databaseConnection,
|
||||
"ALTER TABLE localytics_info ADD install_id CHAR(40)",
|
||||
NULL, NULL, NULL);
|
||||
}
|
||||
|
||||
if (code == SQLITE_OK) {
|
||||
code = sqlite3_exec(_databaseConnection,
|
||||
"ALTER TABLE localytics_info ADD custom_d0 CHAR(64)",
|
||||
NULL, NULL, NULL);
|
||||
}
|
||||
|
||||
if (code == SQLITE_OK) {
|
||||
code = sqlite3_exec(_databaseConnection,
|
||||
"ALTER TABLE localytics_info ADD custom_d1 CHAR(64)",
|
||||
NULL, NULL, NULL);
|
||||
}
|
||||
|
||||
if (code == SQLITE_OK) {
|
||||
code = sqlite3_exec(_databaseConnection,
|
||||
"ALTER TABLE localytics_info ADD custom_d2 CHAR(64)",
|
||||
NULL, NULL, NULL);
|
||||
}
|
||||
|
||||
if (code == SQLITE_OK) {
|
||||
sqlite3_exec(_databaseConnection,
|
||||
"ALTER TABLE localytics_info ADD custom_d3 CHAR(64)",
|
||||
NULL, NULL, NULL);
|
||||
}
|
||||
|
||||
// Attempt to set schema version and install_id regardless of the result code following the ALTER statements above.
|
||||
// This is necessary because a previous version of the library performed the migration without setting these values.
|
||||
// The transaction will succeed even if the individual statements fail with errors (eg. "duplicate column name").
|
||||
sqlite3_stmt *updateLocalyticsInfo;
|
||||
sqlite3_prepare_v2(_databaseConnection, "UPDATE localytics_info set install_id = ?, schema_version = 2 ", -1, &updateLocalyticsInfo, NULL);
|
||||
sqlite3_bind_text (updateLocalyticsInfo, 1, [[self randomUUID] UTF8String], -1, SQLITE_TRANSIENT);
|
||||
code = sqlite3_step(updateLocalyticsInfo);
|
||||
sqlite3_finalize(updateLocalyticsInfo);
|
||||
|
||||
// Commit transaction.
|
||||
if (code == SQLITE_OK || code == SQLITE_DONE) {
|
||||
sqlite3_exec(_databaseConnection, "COMMIT", NULL, NULL, NULL);
|
||||
} else {
|
||||
sqlite3_exec(_databaseConnection, "ROLLBACK", NULL, NULL, NULL);
|
||||
}
|
||||
}
|
||||
|
||||
// V3 adds a field for the last app key and patches a V2 migration issue.
|
||||
- (void)upgradeToSchemaV3 {
|
||||
sqlite3_exec(_databaseConnection,
|
||||
"ALTER TABLE localytics_info ADD app_key CHAR(64)",
|
||||
NULL, NULL, NULL);
|
||||
}
|
||||
|
||||
- (NSUInteger)databaseSize {
|
||||
NSUInteger size = 0;
|
||||
NSDictionary *fileAttributes = [[NSFileManager defaultManager]
|
||||
attributesOfItemAtPath:[LocalyticsDatabase localyticsDatabasePath]
|
||||
error:nil];
|
||||
size = [fileAttributes fileSize];
|
||||
return size;
|
||||
}
|
||||
|
||||
- (int) eventCount {
|
||||
int count = 0;
|
||||
const char *sql = "SELECT count(*) FROM events";
|
||||
sqlite3_stmt *selectEventCount;
|
||||
|
||||
if(sqlite3_prepare_v2(_databaseConnection, sql, -1, &selectEventCount, NULL) == SQLITE_OK)
|
||||
{
|
||||
if(sqlite3_step(selectEventCount) == SQLITE_ROW) {
|
||||
count = sqlite3_column_int(selectEventCount, 0);
|
||||
}
|
||||
}
|
||||
sqlite3_finalize(selectEventCount);
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
- (NSTimeInterval)createdTimestamp {
|
||||
NSTimeInterval timestamp = 0;
|
||||
NSDictionary *fileAttributes = [[NSFileManager defaultManager]
|
||||
attributesOfItemAtPath:[LocalyticsDatabase localyticsDatabasePath]
|
||||
error:nil];
|
||||
timestamp = [[fileAttributes fileCreationDate] timeIntervalSince1970];
|
||||
return timestamp;
|
||||
}
|
||||
|
||||
- (NSTimeInterval)lastSessionStartTimestamp {
|
||||
|
||||
NSTimeInterval lastSessionStart = 0;
|
||||
|
||||
sqlite3_stmt *selectLastSessionStart;
|
||||
sqlite3_prepare_v2(_databaseConnection, "SELECT last_session_start FROM localytics_info", -1, &selectLastSessionStart, NULL);
|
||||
int code = sqlite3_step(selectLastSessionStart);
|
||||
if (code == SQLITE_ROW) {
|
||||
lastSessionStart = sqlite3_column_double(selectLastSessionStart, 0) == 1;
|
||||
}
|
||||
sqlite3_finalize(selectLastSessionStart);
|
||||
|
||||
return lastSessionStart;
|
||||
}
|
||||
|
||||
- (BOOL)setLastsessionStartTimestamp:(NSTimeInterval)timestamp {
|
||||
sqlite3_stmt *updateLastSessionStart;
|
||||
sqlite3_prepare_v2(_databaseConnection, "UPDATE localytics_info SET last_session_start = ?", -1, &updateLastSessionStart, NULL);
|
||||
sqlite3_bind_double(updateLastSessionStart, 1, timestamp);
|
||||
int code = sqlite3_step(updateLastSessionStart);
|
||||
sqlite3_finalize(updateLastSessionStart);
|
||||
|
||||
return code == SQLITE_DONE;
|
||||
}
|
||||
|
||||
- (BOOL)isOptedOut {
|
||||
BOOL optedOut = NO;
|
||||
|
||||
sqlite3_stmt *selectOptOut;
|
||||
sqlite3_prepare_v2(_databaseConnection, "SELECT opt_out FROM localytics_info", -1, &selectOptOut, NULL);
|
||||
int code = sqlite3_step(selectOptOut);
|
||||
if (code == SQLITE_ROW) {
|
||||
optedOut = sqlite3_column_int(selectOptOut, 0) == 1;
|
||||
}
|
||||
sqlite3_finalize(selectOptOut);
|
||||
|
||||
return optedOut;
|
||||
}
|
||||
|
||||
- (BOOL)setOptedOut:(BOOL)optOut {
|
||||
sqlite3_stmt *updateOptedOut;
|
||||
sqlite3_prepare_v2(_databaseConnection, "UPDATE localytics_info SET opt_out = ?", -1, &updateOptedOut, NULL);
|
||||
sqlite3_bind_int(updateOptedOut, 1, optOut);
|
||||
int code = sqlite3_step(updateOptedOut);
|
||||
sqlite3_finalize(updateOptedOut);
|
||||
|
||||
return code == SQLITE_OK;
|
||||
}
|
||||
|
||||
- (NSString *)customDimension:(int)dimension {
|
||||
if(dimension < 0 || dimension > 3) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
NSString *value = nil;
|
||||
NSString *query = [NSString stringWithFormat:@"select custom_d%i from localytics_info", dimension];
|
||||
|
||||
sqlite3_stmt *selectCustomDim;
|
||||
sqlite3_prepare_v2(_databaseConnection, [query UTF8String], -1, &selectCustomDim, NULL);
|
||||
int code = sqlite3_step(selectCustomDim);
|
||||
if (code == SQLITE_ROW && sqlite3_column_text(selectCustomDim, 0)) {
|
||||
value = [NSString stringWithUTF8String:(char *)sqlite3_column_text(selectCustomDim, 0)];
|
||||
}
|
||||
sqlite3_finalize(selectCustomDim);
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
- (BOOL)setCustomDimension:(int)dimension value:(NSString *)value {
|
||||
if(dimension < 0 || dimension > 3) {
|
||||
return false;
|
||||
}
|
||||
|
||||
NSString *query = [NSString stringWithFormat:@"update localytics_info SET custom_d%i = %@",
|
||||
dimension,
|
||||
(value == nil) ? @"null" : [NSString stringWithFormat:@"\"%@\"", value]];
|
||||
|
||||
int code = sqlite3_exec(_databaseConnection, [query UTF8String], NULL, NULL, NULL);
|
||||
|
||||
return code == SQLITE_OK;
|
||||
}
|
||||
|
||||
- (BOOL)incrementLastUploadNumber:(int *)uploadNumber {
|
||||
NSString *t = @"increment_upload_number";
|
||||
int code = SQLITE_OK;
|
||||
|
||||
code = [self beginTransaction:t] ? SQLITE_OK : SQLITE_ERROR;
|
||||
|
||||
if(code == SQLITE_OK) {
|
||||
// Increment value
|
||||
code = sqlite3_exec(_databaseConnection,
|
||||
"UPDATE localytics_info "
|
||||
"SET last_upload_number = (last_upload_number + 1)",
|
||||
NULL, NULL, NULL);
|
||||
}
|
||||
|
||||
if(code == SQLITE_OK) {
|
||||
// Retrieve new value
|
||||
sqlite3_stmt *selectUploadNumber;
|
||||
sqlite3_prepare_v2(_databaseConnection,
|
||||
"SELECT last_upload_number FROM localytics_info",
|
||||
-1, &selectUploadNumber, NULL);
|
||||
code = sqlite3_step(selectUploadNumber);
|
||||
if (code == SQLITE_ROW) {
|
||||
*uploadNumber = sqlite3_column_int(selectUploadNumber, 0);
|
||||
}
|
||||
sqlite3_finalize(selectUploadNumber);
|
||||
}
|
||||
|
||||
if(code == SQLITE_ROW) {
|
||||
[self releaseTransaction:t];
|
||||
} else {
|
||||
[self rollbackTransaction:t];
|
||||
}
|
||||
|
||||
return code == SQLITE_ROW;
|
||||
}
|
||||
|
||||
- (BOOL)incrementLastSessionNumber:(int *)sessionNumber {
|
||||
NSString *t = @"increment_session_number";
|
||||
int code = [self beginTransaction:t] ? SQLITE_OK : SQLITE_ERROR;
|
||||
|
||||
if(code == SQLITE_OK) {
|
||||
// Increment value
|
||||
code = sqlite3_exec(_databaseConnection,
|
||||
"UPDATE localytics_info "
|
||||
"SET last_session_number = (last_session_number + 1)",
|
||||
NULL, NULL, NULL);
|
||||
}
|
||||
|
||||
if(code == SQLITE_OK) {
|
||||
// Retrieve new value
|
||||
sqlite3_stmt *selectSessionNumber;
|
||||
sqlite3_prepare_v2(_databaseConnection,
|
||||
"SELECT last_session_number FROM localytics_info",
|
||||
-1, &selectSessionNumber, NULL);
|
||||
code = sqlite3_step(selectSessionNumber);
|
||||
if (code == SQLITE_ROW && sessionNumber != NULL) {
|
||||
*sessionNumber = sqlite3_column_int(selectSessionNumber, 0);
|
||||
}
|
||||
sqlite3_finalize(selectSessionNumber);
|
||||
}
|
||||
|
||||
if(code == SQLITE_ROW) {
|
||||
[self releaseTransaction:t];
|
||||
} else {
|
||||
[self rollbackTransaction:t];
|
||||
}
|
||||
|
||||
return code == SQLITE_ROW;
|
||||
}
|
||||
|
||||
- (BOOL)addEventWithBlobString:(NSString *)blob {
|
||||
|
||||
int code = SQLITE_OK;
|
||||
sqlite3_stmt *insertEvent;
|
||||
sqlite3_prepare_v2(_databaseConnection, "INSERT INTO events (blob_string) VALUES (?)", -1, &insertEvent, NULL);
|
||||
sqlite3_bind_text(insertEvent, 1, [blob UTF8String], -1, SQLITE_TRANSIENT);
|
||||
code = sqlite3_step(insertEvent);
|
||||
sqlite3_finalize(insertEvent);
|
||||
|
||||
return code == SQLITE_DONE;
|
||||
}
|
||||
|
||||
- (BOOL)addCloseEventWithBlobString:(NSString *)blob {
|
||||
NSString *t = @"add_close_event";
|
||||
BOOL success = [self beginTransaction:t];
|
||||
|
||||
// Add close event.
|
||||
if (success) {
|
||||
success = [self addEventWithBlobString:blob];
|
||||
}
|
||||
|
||||
// Record row id to localytics_info so that it can be removed if the session resumes.
|
||||
if (success) {
|
||||
sqlite3_stmt *updateCloseEvent;
|
||||
sqlite3_prepare_v2(_databaseConnection, "UPDATE localytics_info SET last_close_event = (SELECT event_id FROM events WHERE rowid = ?)", -1, &updateCloseEvent, NULL);
|
||||
sqlite3_int64 lastRow = sqlite3_last_insert_rowid(_databaseConnection);
|
||||
sqlite3_bind_int64(updateCloseEvent, 1, lastRow);
|
||||
int code = sqlite3_step(updateCloseEvent);
|
||||
sqlite3_finalize(updateCloseEvent);
|
||||
success = code == SQLITE_DONE;
|
||||
}
|
||||
|
||||
if (success) {
|
||||
[self releaseTransaction:t];
|
||||
} else {
|
||||
[self rollbackTransaction:t];
|
||||
}
|
||||
return success;
|
||||
}
|
||||
|
||||
- (BOOL)addFlowEventWithBlobString:(NSString *)blob {
|
||||
NSString *t = @"add_flow_event";
|
||||
BOOL success = [self beginTransaction:t];
|
||||
|
||||
// Add flow event.
|
||||
if (success) {
|
||||
success = [self addEventWithBlobString:blob];
|
||||
}
|
||||
|
||||
// Record row id to localytics_info so that it can be removed if the session resumes.
|
||||
if (success) {
|
||||
sqlite3_stmt *updateFlowEvent;
|
||||
sqlite3_prepare_v2(_databaseConnection, "UPDATE localytics_info SET last_flow_event = (SELECT event_id FROM events WHERE rowid = ?)", -1, &updateFlowEvent, NULL);
|
||||
sqlite3_int64 lastRow = sqlite3_last_insert_rowid(_databaseConnection);
|
||||
sqlite3_bind_int64(updateFlowEvent, 1, lastRow);
|
||||
int code = sqlite3_step(updateFlowEvent);
|
||||
sqlite3_finalize(updateFlowEvent);
|
||||
success = code == SQLITE_DONE;
|
||||
}
|
||||
|
||||
if (success) {
|
||||
[self releaseTransaction:t];
|
||||
} else {
|
||||
[self rollbackTransaction:t];
|
||||
}
|
||||
return success;
|
||||
}
|
||||
|
||||
- (BOOL)removeLastCloseAndFlowEvents {
|
||||
// Attempt to remove the last recorded close event.
|
||||
// Fail quietly if none was saved or it was previously removed.
|
||||
int code = sqlite3_exec(_databaseConnection, "DELETE FROM events WHERE event_id = (SELECT last_close_event FROM localytics_info) OR event_id = (SELECT last_flow_event FROM localytics_info)", NULL, NULL, NULL);
|
||||
|
||||
return code == SQLITE_OK;
|
||||
}
|
||||
|
||||
- (BOOL)addHeaderWithSequenceNumber:(int)number blobString:(NSString *)blob rowId:(sqlite3_int64 *)insertedRowId {
|
||||
sqlite3_stmt *insertHeader;
|
||||
sqlite3_prepare_v2(_databaseConnection, "INSERT INTO upload_headers (sequence_number, blob_string) VALUES (?, ?)", -1, &insertHeader, NULL);
|
||||
sqlite3_bind_int(insertHeader, 1, number);
|
||||
sqlite3_bind_text(insertHeader, 2, [blob UTF8String], -1, SQLITE_TRANSIENT);
|
||||
int code = sqlite3_step(insertHeader);
|
||||
sqlite3_finalize(insertHeader);
|
||||
|
||||
if (code == SQLITE_DONE && insertedRowId != NULL) {
|
||||
*insertedRowId = sqlite3_last_insert_rowid(_databaseConnection);
|
||||
}
|
||||
|
||||
return code == SQLITE_DONE;
|
||||
}
|
||||
|
||||
- (int)unstagedEventCount {
|
||||
int rowCount = 0;
|
||||
sqlite3_stmt *selectEventCount;
|
||||
sqlite3_prepare_v2(_databaseConnection, "SELECT COUNT(*) FROM events WHERE UPLOAD_HEADER IS NULL", -1, &selectEventCount, NULL);
|
||||
int code = sqlite3_step(selectEventCount);
|
||||
if (code == SQLITE_ROW) {
|
||||
rowCount = sqlite3_column_int(selectEventCount, 0);
|
||||
}
|
||||
sqlite3_finalize(selectEventCount);
|
||||
|
||||
return rowCount;
|
||||
}
|
||||
|
||||
- (BOOL)stageEventsForUpload:(sqlite3_int64)headerId {
|
||||
|
||||
// Associate all outstanding events with the given upload header ID.
|
||||
NSString *stageEvents = [NSString stringWithFormat:@"UPDATE events SET upload_header = ? WHERE upload_header IS NULL"];
|
||||
sqlite3_stmt *updateEvents;
|
||||
sqlite3_prepare_v2(_databaseConnection, [stageEvents UTF8String], -1, &updateEvents, NULL);
|
||||
sqlite3_bind_int(updateEvents, 1, headerId);
|
||||
int code = sqlite3_step(updateEvents);
|
||||
sqlite3_finalize(updateEvents);
|
||||
BOOL success = (code == SQLITE_DONE);
|
||||
|
||||
return success;
|
||||
}
|
||||
|
||||
- (BOOL)updateAppKey:(NSString *)appKey {
|
||||
sqlite3_stmt *updateAppKey;
|
||||
sqlite3_prepare_v2(_databaseConnection, "UPDATE localytics_info set app_key = ?", -1, &updateAppKey, NULL);
|
||||
sqlite3_bind_text (updateAppKey, 1, [appKey UTF8String], -1, SQLITE_TRANSIENT);
|
||||
int code = sqlite3_step(updateAppKey);
|
||||
sqlite3_finalize(updateAppKey);
|
||||
BOOL success = (code == SQLITE_DONE);
|
||||
|
||||
return success;
|
||||
}
|
||||
|
||||
- (NSString *)uploadBlobString {
|
||||
|
||||
// Retrieve the blob strings of each upload header and its child events, in order.
|
||||
const char *sql = "SELECT * FROM ( "
|
||||
" SELECT h.blob_string AS 'blob', h.sequence_number as 'seq', 0 FROM upload_headers h"
|
||||
" UNION ALL "
|
||||
" SELECT e.blob_string AS 'blob', e.upload_header as 'seq', 1 FROM events e"
|
||||
") "
|
||||
"ORDER BY 2, 3";
|
||||
sqlite3_stmt *selectBlobs;
|
||||
sqlite3_prepare_v2(_databaseConnection, sql, -1, &selectBlobs, NULL);
|
||||
NSMutableString *uploadBlobString = [NSMutableString string];
|
||||
while (sqlite3_step(selectBlobs) == SQLITE_ROW) {
|
||||
const char *blob = (const char *)sqlite3_column_text(selectBlobs, 0);
|
||||
if (blob != NULL) {
|
||||
NSString *blobString = [[NSString alloc] initWithCString:blob encoding:NSUTF8StringEncoding];
|
||||
[uploadBlobString appendString:blobString];
|
||||
[blobString release];
|
||||
}
|
||||
}
|
||||
sqlite3_finalize(selectBlobs);
|
||||
|
||||
return [[uploadBlobString copy] autorelease];
|
||||
}
|
||||
|
||||
- (BOOL)deleteUploadedData {
|
||||
// Delete all headers and staged events.
|
||||
NSString *t = @"delete_upload_data";
|
||||
int code = [self beginTransaction:t] ? SQLITE_OK : SQLITE_ERROR;
|
||||
|
||||
if (code == SQLITE_OK) {
|
||||
code = sqlite3_exec(_databaseConnection, "DELETE FROM events WHERE upload_header IS NOT NULL", NULL, NULL, NULL);
|
||||
}
|
||||
|
||||
if (code == SQLITE_OK) {
|
||||
code = sqlite3_exec(_databaseConnection, "DELETE FROM upload_headers", NULL, NULL, NULL);
|
||||
}
|
||||
|
||||
if (code == SQLITE_OK) {
|
||||
[self releaseTransaction:t];
|
||||
} else {
|
||||
[self rollbackTransaction:t];
|
||||
}
|
||||
|
||||
return code == SQLITE_OK;
|
||||
}
|
||||
|
||||
- (BOOL)resetAnalyticsData {
|
||||
// Delete or zero all analytics data.
|
||||
// Reset: headers, events, session number, upload number, last session start, last close event, and last flow event.
|
||||
// Unaffected: schema version, opt out status, install ID (deprecated), and app key.
|
||||
|
||||
NSString *t = @"reset_analytics_data";
|
||||
int code = [self beginTransaction:t] ? SQLITE_OK : SQLITE_ERROR;
|
||||
|
||||
if (code == SQLITE_OK) {
|
||||
code = sqlite3_exec(_databaseConnection, "DELETE FROM events", NULL, NULL, NULL);
|
||||
}
|
||||
|
||||
if (code == SQLITE_OK) {
|
||||
code = sqlite3_exec(_databaseConnection, "DELETE FROM upload_headers", NULL, NULL, NULL);
|
||||
}
|
||||
|
||||
if (code == SQLITE_OK) {
|
||||
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, "
|
||||
"custom_d0 = null, custom_d1 = null, custom_d2 = null, custom_d3 = null",
|
||||
NULL, NULL, NULL);
|
||||
}
|
||||
|
||||
if (code == SQLITE_OK) {
|
||||
[self releaseTransaction:t];
|
||||
} else {
|
||||
[self rollbackTransaction:t];
|
||||
}
|
||||
|
||||
return code == SQLITE_OK;
|
||||
}
|
||||
|
||||
- (BOOL)vacuumIfRequired {
|
||||
int code = SQLITE_OK;
|
||||
if ([self databaseSize] > MAX_DATABASE_SIZE * VACUUM_THRESHOLD) {
|
||||
code = sqlite3_exec(_databaseConnection, "VACUUM", NULL, NULL, NULL);
|
||||
}
|
||||
|
||||
return code == SQLITE_OK;
|
||||
}
|
||||
|
||||
- (NSString *)randomUUID {
|
||||
CFUUIDRef theUUID = CFUUIDCreate(NULL);
|
||||
CFStringRef stringUUID = CFUUIDCreateString(NULL, theUUID);
|
||||
CFRelease(theUUID);
|
||||
return [(NSString *)stringUUID autorelease];
|
||||
}
|
||||
|
||||
#pragma mark - Lifecycle
|
||||
|
||||
+ (id)allocWithZone:(NSZone *)zone {
|
||||
@synchronized(self) {
|
||||
if (_sharedLocalyticsDatabase == nil) {
|
||||
_sharedLocalyticsDatabase = [super allocWithZone:zone];
|
||||
return _sharedLocalyticsDatabase;
|
||||
}
|
||||
}
|
||||
// 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 {
|
||||
sqlite3_close(_databaseConnection);
|
||||
[super dealloc];
|
||||
}
|
||||
|
||||
@end
|
216
Localytics/LocalyticsSession.h
Normal file
216
Localytics/LocalyticsSession.h
Normal file
@@ -0,0 +1,216 @@
|
||||
// LocalyticsSession.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>
|
||||
|
||||
// Set this to true to enable localytics traces (useful for debugging)
|
||||
#define DO_LOCALYTICS_LOGGING false
|
||||
|
||||
/*!
|
||||
@class LocalyticsSession
|
||||
@discussion The class which manages creating, collecting, & uploading a Localytics session.
|
||||
Please see the following guides for information on how to best use this
|
||||
library, sample code, and other useful information:
|
||||
<ul>
|
||||
<li><a href="http://wiki.localytics.com/index.php?title=Developer's_Integration_Guide">Main Developer's Integration Guide</a></li>
|
||||
</ul>
|
||||
|
||||
<strong>Best Practices</strong>
|
||||
<ul>
|
||||
<li>Instantiate the LocalyticsSession object in applicationDidFinishLaunching.</li>
|
||||
<li>Open your session and begin your uploads in applicationDidFinishLaunching. This way the
|
||||
upload has time to complete and it all happens before your users have a
|
||||
chance to begin any data intensive actions of their own.</li>
|
||||
<li>Close the session in applicationWillTerminate, and in applicationDidEnterBackground.</li>
|
||||
<li>Resume the session in applicationWillEnterForeground.</li>
|
||||
<li>Do not call any Localytics functions inside a loop. Instead, calls
|
||||
such as <code>tagEvent</code> should follow user actions. This limits the
|
||||
amount of data which is stored and uploaded.</li>
|
||||
<li>Do not use multiple LocalticsSession objects to upload data with
|
||||
multiple application keys. This can cause invalid state.</li>
|
||||
</ul>
|
||||
|
||||
@author Localytics
|
||||
*/
|
||||
@interface LocalyticsSession : NSObject {
|
||||
|
||||
BOOL _hasInitialized; // Whether or not the session object has been initialized.
|
||||
BOOL _isSessionOpen; // Whether or not this session has been opened.
|
||||
float _backgroundSessionTimeout; // If an App stays in the background for more
|
||||
// than this many seconds, start a new session
|
||||
// when it returns to foreground.
|
||||
@private
|
||||
#pragma mark Member Variables
|
||||
dispatch_queue_t _queue; // Queue of Localytics block objects.
|
||||
dispatch_group_t _criticalGroup; // Group of blocks the must complete before backgrounding.
|
||||
NSString *_sessionUUID; // Unique identifier for this session.
|
||||
NSString *_applicationKey; // Unique identifier for the instrumented application
|
||||
NSTimeInterval _lastSessionStartTimestamp; // The start time of the most recent session.
|
||||
NSDate *_sessionResumeTime; // Time session was started or resumed.
|
||||
NSDate *_sessionCloseTime; // Time session was closed.
|
||||
NSMutableString *_unstagedFlowEvents; // Comma-delimited list of app screens and events tagged during this
|
||||
// session that have NOT been staged for upload.
|
||||
NSMutableString *_stagedFlowEvents; // App screens and events tagged during this session that HAVE been staged
|
||||
// for upload.
|
||||
NSMutableString *_screens; // Comma-delimited list of screens tagged during this session.
|
||||
NSTimeInterval _sessionActiveDuration; // Duration that session open.
|
||||
BOOL _sessionHasBeenOpen; // Whether or not this session has ever been open.
|
||||
}
|
||||
|
||||
@property dispatch_queue_t queue;
|
||||
@property dispatch_group_t criticalGroup;
|
||||
@property BOOL isSessionOpen;
|
||||
@property BOOL hasInitialized;
|
||||
@property float backgroundSessionTimeout;
|
||||
|
||||
#pragma mark Public Methods
|
||||
/*!
|
||||
@method sharedLocalyticsSession
|
||||
@abstract Accesses the Session object. This is a Singleton class which maintains
|
||||
a single session throughout your application. It is possible to manage your own
|
||||
session, but this is the easiest way to access the Localytics object throughout your code.
|
||||
The class is accessed within the code using the following syntax:
|
||||
[[LocalyticsSession sharedLocalyticsSession] functionHere]
|
||||
So, to tag an event, all that is necessary, anywhere in the code is:
|
||||
[[LocalyticsSession sharedLocalyticsSession] tagEvent:@"MY_EVENT"];
|
||||
*/
|
||||
+ (LocalyticsSession *)sharedLocalyticsSession;
|
||||
|
||||
/*!
|
||||
@method LocalyticsSession
|
||||
@abstract Initializes the Localytics Object. Not necessary if you choose to use startSession.
|
||||
@param applicationKey The key unique for each application generated at www.localytics.com
|
||||
*/
|
||||
- (void)LocalyticsSession:(NSString *)appKey;
|
||||
|
||||
/*!
|
||||
@method startSession
|
||||
@abstract An optional convenience initialize method that also calls the LocalyticsSession, open &
|
||||
upload methods. Best Practice is to call open & upload immediately after Localytics Session when loading an app,
|
||||
this method fascilitates that behavior.
|
||||
It is recommended that this call be placed in <code>applicationDidFinishLaunching</code>.
|
||||
@param applicationKey The key unique for each application generated
|
||||
at www.localytics.com
|
||||
*/
|
||||
- (void)startSession:(NSString *)appKey;
|
||||
|
||||
/*!
|
||||
@method setOptIn
|
||||
@abstract (OPTIONAL) Allows the application to control whether or not it will collect user data.
|
||||
Even if this call is used, it is necessary to continue calling upload(). No new data will be
|
||||
collected, so nothing new will be uploaded but it is necessary to upload an event telling the
|
||||
server this user has opted out.
|
||||
@param optedIn True if the user is opted in, false otherwise.
|
||||
*/
|
||||
- (void)setOptIn:(BOOL)optedIn;
|
||||
|
||||
/*!
|
||||
@method isOptedIn
|
||||
@abstract (OPTIONAL) Whether or not this user has is opted in or out. The only way they can be
|
||||
opted out is if setOptIn(false) has been called before this. This function should only be
|
||||
used to pre-populate a checkbox in an options menu. It is not recommended that an application
|
||||
branch based on Localytics instrumentation because this creates an additional test case. If
|
||||
the app is opted out, all subsequent Localytics calls will return immediately.
|
||||
@result true if the user is opted in, false otherwise.
|
||||
*/
|
||||
- (BOOL)isOptedIn;
|
||||
|
||||
/*!
|
||||
@method open
|
||||
@abstract Opens the Localytics session. Not necessary if you choose to use startSession.
|
||||
The session time as presented on the website is the time between <code>open</code> and the
|
||||
final <code>close</code> so it is recommended to open the session as early as possible, and close
|
||||
it at the last moment. The session must be opened before any tags can
|
||||
be written. It is recommended that this call be placed in <code>applicationDidFinishLaunching</code>.
|
||||
<br>
|
||||
If for any reason this is called more than once every subsequent open call
|
||||
will be ignored.
|
||||
*/
|
||||
- (void)open;
|
||||
|
||||
/*!
|
||||
@method resume
|
||||
@abstract Resumes the Localytics session. When the App enters the background, the session is
|
||||
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
|
||||
by default) a new session is created, and uploading is triggered. Otherwise, the previous session
|
||||
is reopened.
|
||||
*/
|
||||
- (void)resume;
|
||||
|
||||
/*!
|
||||
@method close
|
||||
@abstract Closes the Localytics session. This should be called in
|
||||
<code>applicationWillTerminate</code>.
|
||||
<br>
|
||||
If close is not called, the session will still be uploaded but no
|
||||
events will be processed and the session time will not appear. This is
|
||||
because the session is not yet closed so it should not be used in
|
||||
comparison with sessions which are closed.
|
||||
*/
|
||||
- (void)close;
|
||||
|
||||
/*!
|
||||
@method tagEvent
|
||||
@abstract Allows a session to tag a particular event as having occurred. For
|
||||
example, if a view has three buttons, it might make sense to tag
|
||||
each button click with the name of the button which was clicked.
|
||||
For another example, in a game with many levels it might be valuable
|
||||
to create a new tag every time the user gets to a new level in order
|
||||
to determine how far the average user is progressing in the game.
|
||||
<br>
|
||||
<strong>Tagging Best Practices</strong>
|
||||
<ul>
|
||||
<li>DO NOT use tags to record personally identifiable information.</li>
|
||||
<li>The best way to use tags is to create all the tag strings as predefined
|
||||
constants and only use those. This is more efficient and removes the risk of
|
||||
collecting personal information.</li>
|
||||
<li>Do not set tags inside loops or any other place which gets called
|
||||
frequently. This can cause a lot of data to be stored and uploaded.</li>
|
||||
</ul>
|
||||
<br>
|
||||
See the tagging guide at: http://wiki.localytics.com/
|
||||
@param event The name of the event which occurred.
|
||||
*/
|
||||
- (void)tagEvent:(NSString *)event;
|
||||
|
||||
- (void)tagEvent:(NSString *)event attributes:(NSDictionary *)attributes;
|
||||
|
||||
- (void)tagEvent:(NSString *)event attributes:(NSDictionary *)attributes reportAttributes:(NSDictionary *)reportAttributes;
|
||||
|
||||
/*!
|
||||
@method tagScreen
|
||||
@abstract Allows tagging the flow of screens encountered during the session.
|
||||
@param screen The name of the screen
|
||||
*/
|
||||
- (void)tagScreen:(NSString *)screen;
|
||||
|
||||
/*!
|
||||
@method upload
|
||||
@abstract Creates a low priority thread which uploads any Localytics data already stored
|
||||
on the device. This should be done early in the process life in order to
|
||||
guarantee as much time as possible for slow connections to complete. It is also reasonable
|
||||
to upload again when the application is exiting because if the upload is cancelled the data
|
||||
will just get uploaded the next time the app comes up.
|
||||
*/
|
||||
- (void)upload;
|
||||
|
||||
/*!
|
||||
@method setCustomDimension
|
||||
@abstract (ENTERPRISE ONLY) Sets the value of a custom dimension. Custom dimensions are dimensions
|
||||
which contain user defined data unlike the predefined dimensions such as carrier, model, and country.
|
||||
Once a value for a custom dimension is set, the device it was set on will continue to upload that value
|
||||
until the value is changed. To clear a value pass nil as the value.
|
||||
The proper use of custom dimensions involves defining a dimension with less than ten distinct possible
|
||||
values and assigning it to one of the four available custom dimensions. Once assigned this definition should
|
||||
never be changed without changing the App Key otherwise old installs of the application will pollute new data.
|
||||
*/
|
||||
- (void)setCustomDimension:(int)dimension value:(NSString *)value;
|
||||
|
||||
@end
|
1148
Localytics/LocalyticsSession.m
Normal file
1148
Localytics/LocalyticsSession.m
Normal file
File diff suppressed because it is too large
Load Diff
42
Localytics/LocalyticsUploader.h
Normal file
42
Localytics/LocalyticsUploader.h
Normal file
@@ -0,0 +1,42 @@
|
||||
// LocalyticsUploader.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 LocalyticsUploader
|
||||
@discussion Singleton class to handle data uploads
|
||||
*/
|
||||
|
||||
@interface LocalyticsUploader : NSObject {
|
||||
}
|
||||
|
||||
@property (readonly) BOOL isUploading;
|
||||
|
||||
/*!
|
||||
@method sharedLocalyticsUploader
|
||||
@abstract Establishes this as a Singleton Class allowing for data persistence.
|
||||
The class is accessed within the code using the following syntax:
|
||||
[[LocalyticsUploader sharedLocalyticsUploader] functionHere]
|
||||
*/
|
||||
+ (LocalyticsUploader *)sharedLocalyticsUploader;
|
||||
|
||||
/*!
|
||||
@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
|
||||
*/
|
||||
- (void)uploaderWithApplicationKey:(NSString *)localyticsApplicationKey;
|
||||
|
||||
@end
|
236
Localytics/LocalyticsUploader.m
Normal file
236
Localytics/LocalyticsUploader.m
Normal file
@@ -0,0 +1,236 @@
|
||||
// LocalyticsUploader.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 "LocalyticsUploader.h"
|
||||
#import "LocalyticsSession.h"
|
||||
#import "LocalyticsDatabase.h"
|
||||
#import <zlib.h>
|
||||
|
||||
#define LOCALYTICS_URL @"http://analytics.localytics.com/api/v2/applications/%@/uploads"
|
||||
|
||||
static LocalyticsUploader *_sharedUploader = nil;
|
||||
|
||||
@interface LocalyticsUploader ()
|
||||
- (void)finishUpload;
|
||||
- (NSData *)gzipDeflatedDataWithData:(NSData *)data;
|
||||
- (void)logMessage:(NSString *)message;
|
||||
|
||||
@property (readwrite) BOOL isUploading;
|
||||
|
||||
@end
|
||||
|
||||
@implementation LocalyticsUploader
|
||||
@synthesize isUploading = _isUploading;
|
||||
|
||||
#pragma mark - Singleton Class
|
||||
+ (LocalyticsUploader *)sharedLocalyticsUploader {
|
||||
@synchronized(self) {
|
||||
if (_sharedUploader == nil) {
|
||||
_sharedUploader = [[self alloc] init];
|
||||
}
|
||||
}
|
||||
return _sharedUploader;
|
||||
}
|
||||
|
||||
#pragma mark - Class Methods
|
||||
|
||||
- (void)uploaderWithApplicationKey:(NSString *)localyticsApplicationKey {
|
||||
|
||||
// Do nothing if already uploading.
|
||||
if (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 finishUpload];
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
NSData *requestData = [blobString dataUsingEncoding:NSUTF8StringEncoding];
|
||||
NSString *myString = [[[NSString alloc] initWithData:requestData encoding:NSUTF8StringEncoding] autorelease];
|
||||
[self logMessage:[NSString stringWithFormat:@"Uploading data (length: %u)", [myString length]]];
|
||||
|
||||
// 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];
|
||||
|
||||
// Perform synchronous upload in an async dispatch. This is necessary because the calling block will not persist to
|
||||
// receive the response data.
|
||||
dispatch_group_async([[LocalyticsSession sharedLocalyticsSession] criticalGroup], [[LocalyticsSession sharedLocalyticsSession] queue], ^{
|
||||
@try {
|
||||
NSURLResponse *response = nil;
|
||||
NSError *responseError = nil;
|
||||
[NSURLConnection sendSynchronousRequest:submitRequest returningResponse:&response error:&responseError];
|
||||
NSInteger responseStatusCode = [(NSHTTPURLResponse *)response statusCode];
|
||||
|
||||
if (responseError) {
|
||||
// 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: %@",
|
||||
[responseError code],
|
||||
[responseError localizedDescription]]];
|
||||
} else {
|
||||
// Step 3
|
||||
// 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 {
|
||||
// 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] deleteUploadedData];
|
||||
}
|
||||
}
|
||||
}
|
||||
@catch (NSException * e) {}
|
||||
|
||||
[self finishUpload];
|
||||
});
|
||||
}
|
||||
|
||||
- (void)finishUpload
|
||||
{
|
||||
self.isUploading = false;
|
||||
|
||||
// Upload data has been deleted. Recover the disk space if necessary.
|
||||
[[LocalyticsDatabase sharedLocalyticsDatabase] vacuumIfRequired];
|
||||
}
|
||||
|
||||
/*!
|
||||
@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 (_sharedUploader == nil) {
|
||||
_sharedUploader = [super allocWithZone:zone];
|
||||
return _sharedUploader;
|
||||
}
|
||||
}
|
||||
// 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 {
|
||||
[_sharedUploader release];
|
||||
[super dealloc];
|
||||
}
|
||||
|
||||
@end
|
48
Localytics/UploaderThread.h
Normal file
48
Localytics/UploaderThread.h
Normal file
@@ -0,0 +1,48 @@
|
||||
// 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
|
260
Localytics/UploaderThread.m
Normal file
260
Localytics/UploaderThread.m
Normal file
@@ -0,0 +1,260 @@
|
||||
// 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
|
111
Localytics/WebserviceConstants.h
Normal file
111
Localytics/WebserviceConstants.h
Normal file
@@ -0,0 +1,111 @@
|
||||
// WebserviceConstants.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.
|
||||
|
||||
// The constants which are used to make up the JSON blob
|
||||
// To save disk space and network bandwidth all the keywords have been
|
||||
// abbreviated and are exploded by the server.
|
||||
|
||||
/*********************
|
||||
* Shared Attributes *
|
||||
*********************/
|
||||
#define PARAM_UUID @"u" // UUID for JSON document
|
||||
#define PARAM_DATA_TYPE @"dt" // Data Type
|
||||
#define PARAM_CLIENT_TIME @"ct" // Client Time, seconds from Unix epoch (int)
|
||||
#define PARAM_LATITUDE @"lat" // Latitude - if available
|
||||
#define PARAM_LONGITUDE @"lon" // Longitude - if available
|
||||
#define PARAM_SESSION_UUID @"su" // UUID for an existing session
|
||||
#define PARAM_NEW_SESSION_UUID @"u" // UUID for a new session
|
||||
#define PARAM_ATTRIBUTES @"attrs" // Attributes (dictionary)
|
||||
|
||||
/***************
|
||||
* Blob Header *
|
||||
***************/
|
||||
|
||||
// PARAM_UUID
|
||||
// PARAM_DATA_TYPE => "h" for Header
|
||||
// PARAM_ATTRIBUTES => dictionary containing Header Common Attributes
|
||||
#define PARAM_PERSISTED_AT @"pa" // Persistent Storage Created At. A timestamp created when the app was
|
||||
// first launched and the persistent storage was created. Stores as
|
||||
// seconds from Unix epoch. (int)
|
||||
#define PARAM_SEQUENCE_NUMBER @"seq" // Sequence number - an increasing count for each blob, stored in the
|
||||
// persistent store Consistent across app starts. (int)
|
||||
|
||||
/****************************
|
||||
* Header Common Attributes *
|
||||
****************************/
|
||||
|
||||
// PARAM_DATA_TYPE
|
||||
#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_MAC @"wmac" // Hashed version of the device Mac
|
||||
#define PARAM_INSTALL_ID @"iu" // Install ID
|
||||
#define PARAM_JAILBROKEN @"j" // Jailbroken (boolean)
|
||||
#define PARAM_LIBRARY_VERSION @"lv" // Client Version
|
||||
#define PARAM_APP_VERSION @"av" // Application Version
|
||||
#define PARAM_DEVICE_PLATFORM @"dp" // Device Platform
|
||||
#define PARAM_LOCALE_LANGUAGE @"dll" // Locale Language
|
||||
#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_MANUFACTURER @"dma" // Device Manufacturer // ???: Never used on iPhone. Used to be "Device Make".
|
||||
#define PARAM_DEVICE_MODEL @"dmo" // Device Model
|
||||
#define PARAM_DEVICE_OS_VERSION @"dov" // Device OS Version
|
||||
#define PARAM_NETWORK_CARRIER @"nca" // Network Carrier
|
||||
#define PARAM_DATA_CONNECTION @"dac" // Data Connection Type // ???: Never used on iPhone.
|
||||
#define PARAM_OPT_VALUE @"optin" // Opt In (boolean)
|
||||
#define PARAM_DEVICE_MEMORY @"dmem" // Device Memory
|
||||
|
||||
/*****************
|
||||
* Session Start *
|
||||
*****************/
|
||||
|
||||
// PARAM_UUID
|
||||
// PARAM_DATA_TYPE => "s" for Start
|
||||
// PARAM_CLIENT_TIME
|
||||
#define PARAM_SESSION_NUMBER @"nth" // This is the nth session on the device, 1-indexed (int)
|
||||
|
||||
/****************
|
||||
* Session Stop *
|
||||
****************/
|
||||
|
||||
// PARAM_UUID
|
||||
// PARAM_DATA_TYPE => "c" for Close
|
||||
// PARAM_CLIENT_TIME
|
||||
// PARAM_LATITUDE
|
||||
// PARAM_LONGITUDE
|
||||
// PARAM_SESSION_UUID => UUID of session being closed
|
||||
#define PARAM_SESSION_ACTIVE @"cta" // Active time in seconds (time app was active)
|
||||
#define PARAM_SESSION_TOTAL @"ctl" // Total session length
|
||||
#define PARAM_SESSION_SCREENFLOW @"fl" // Screens encountered during this session, in order
|
||||
|
||||
/*********************
|
||||
* Application Event *
|
||||
*********************/
|
||||
|
||||
// PARAM_UUID
|
||||
// PARAM_DATA_TYPE => "e" for Event
|
||||
// PARAM_CLIENT_TIME
|
||||
// PARAM_LATITUDE
|
||||
// PARAM_LONGITUDE
|
||||
// PARAM_SESSION_UUID => UUID of session event occured in
|
||||
// PARAM_ATTRIBUTES => dictionary containing attributes for this event as key-value string pairs
|
||||
#define PARAM_EVENT_NAME @"n" // Event Name, (eg. 'Button Click')
|
||||
#define PARAM_REPORT_ATTRIBUTES @"rattrs" // Attributes used in custom reports
|
||||
|
||||
/********************
|
||||
* Application flow *
|
||||
********************/
|
||||
|
||||
// PARAM_UUID
|
||||
// PARAM_DATA_TYPE => "f" for Flow
|
||||
// PARAM_CLIENT_TIME
|
||||
#define PARAM_SESSION_START @"ss" // Start time for the current session.
|
||||
#define PARAM_NEW_FLOW_EVENTS @"nw" // Events and screens encountered during this session that have NOT been staged for upload.
|
||||
#define PARAM_OLD_FLOW_EVENTS @"od" // Events and screens encountered during this session that HAVE been staged for upload.
|
Reference in New Issue
Block a user