Question

I'm developing an iOS app and am planning to use an analytics service in it, like Flurry or Google Analytics. The thing is: what would be a good software design (loose coupled, highly cohesive, easy to maintain) to use these services?

This is a personal project of mine and I'm using it to study new technologies and best practices. I stumbled into this "challenge" and can't really find one best solution.

I have already developed some mobile apps that use this kind of service and, usually, I implement a combo of the Adapter + Factory design patterns:

  • A basic interface that represents a generic analytics service is created

    public interface IAnalytics {
        void LogEvent(string name, object param);
    }
    
  • Each Service (Flurry/Google Analytics/etc) API is encapsulated through the use of an adapter that implements this interface

    public class FlurryService : IAnalyticsService {
        public LogEvent(sring name, object param) {
            Flurry.Log(name, param.ToString());
        }
    } 
    
  • A factory is implemented so that we have the whatever analytics service we need on that particular application

    public static class AnalyticsServiceFactory {
        public IAnalytics CreateService(string servicename) {
            if(servicename == "google") {
                return new GoogleAnalyticsService();
            } else {
                return new FlurryService();
            }
        }
    }
    
  • At last, a "proxy" object (not by the book) is created to log application-specific events

    public class MyAnalytics {
        private static IAnalyticsService _Service = AnalyticsServiceFactory.CreateService("flurry");
    
        public static void LogUserLoggedIn(string user) {
            _Service.LogEvent("login", user);
        }
    
        public static void LogLoginFailed(string user) {
            _Service.LogEvent("login", user);
        }
    }
    

This deals with encapsulation of each service API and works great, specially in apps that share code between different platforms.

There is however one problem left that is the logging of events (or actions done by the user) itself. In all cases I've worked on, the logging of events is hardcoded wherever such event occurs. For example:

public void LogIn(string userName, string pass) {
    bool success = this.Controller.LogIn(userName, pass);

    if(success) {
        MyAnalytics.LogUserLoggedIn(username);

        // Change view

    } else {
        MyAnalytics.LogLogInFailed(username);

        // Alert
    }
}

This seems more coupled than what I'd like it to be, so I'm searching for a better solution.

As I'm working with iOS, I thought about using NSNotificationCenter: Whenever an event happens, instead of immediately logging it, I post a notification in the NSNotificationCenter and another object observing these notifications takes care of calling MyAnalytics to log the event. This design, however, only works with iOS (without a non-trivial ammount of work, that is).

Another way to look at this problem is: How is it that games track your actions in order to reach an Xbox Achievement/Playstation Trophy?

Was it helpful?

Solution

Here's a design I typically use for a logging class that can be multi-provider. It allows for easy swaps between analytics, crash reporting and beta OTA providers and it uses preprocessor directives so that certain services are active in certain environments. This example uses CocoaLumberjack but you could make it work with your logging framework of choice.

@class CLLocation;

@interface TGLogger : NSObject

+ (void)startLogging;
+ (void)stopLogging;

+ (NSString *)getUdidKey;

+ (void)setUserID:(NSString *)userID;
+ (void)setUsername:(NSString *)username;
+ (void)setUserEmail:(NSString *)email;
+ (void)setUserAge:(NSUInteger)age;
+ (void)setUserGender:(NSString *)gender;
+ (void)setUserLocation:(CLLocation *)location;
+ (void)setUserValue:(NSString *)value forKey:(NSString *)key;
+ (void)setIntValue:(int)value forKey:(NSString *)key;
+ (void)setFloatValue:(float)value forKey:(NSString *)key;
+ (void)setBoolValue:(BOOL)value forKey:(NSString *)key;

extern void TGReportMilestone(NSString *milestone, NSDictionary *parameters);
extern void TGReportBeginTimedMilestone(NSString *milestone, NSDictionary *parameters);
extern void TGReportEndTimedMilestone(NSString *milestone, NSDictionary *parameters);

@end

Now the implementation file:

#import "TGLogger.h"
#import "TGAppDelegate.h"
#import <CocoaLumberjack/DDASLLogger.h>
#import <CocoaLumberjack/DDTTYLogger.h>
#import <AFNetworkActivityLogger/AFNetworkActivityLogger.h>
@import CoreLocation;

#ifdef USE_CRASHLYTICS
#import <Crashlytics/Crashlytics.h>
#import <CrashlyticsLumberjack/CrashlyticsLogger.h>
#endif

#ifdef USE_FLURRY
#import <FlurrySDK/Flurry.h>
#endif

#import <Flurry.h>

@implementation TGLogger

+ (void)startLogging
{
    [DDLog addLogger:[DDASLLogger sharedInstance]];
    [DDLog addLogger:[DDTTYLogger sharedInstance]];
    [[DDTTYLogger sharedInstance] setColorsEnabled:YES];
    [[DDTTYLogger sharedInstance] setForegroundColor:[UIColor blueColor] backgroundColor:nil forFlag:LOG_FLAG_INFO];
    [[DDTTYLogger sharedInstance] setForegroundColor:[UIColor orangeColor] backgroundColor:nil forFlag:LOG_FLAG_WARN];
    [[DDTTYLogger sharedInstance] setForegroundColor:[UIColor redColor] backgroundColor:nil forFlag:LOG_FLAG_ERROR];

    [[AFNetworkActivityLogger sharedLogger] startLogging];

#ifdef DEBUG
    [[AFNetworkActivityLogger sharedLogger] setLevel:AFLoggerLevelInfo];
#else
    [[AFNetworkActivityLogger sharedLogger] setLevel:AFLoggerLevelWarn];
#endif


#if defined(USE_CRASHLYTICS) || defined(USE_FLURRY)
    NSString *udid = [TGLogger getUdidKey];
    TGLogInfo(@"Current UDID is: %@", udid);
#endif

#ifdef USE_CRASHLYTICS
    // Start Crashlytics
    [Crashlytics startWithAPIKey:TGCrashlyticsKey];
    [Crashlytics setUserIdentifier:udid];
    [DDLog addLogger:[CrashlyticsLogger sharedInstance]];
    TGLogInfo(@"Crashlytics started with API Key: %@", TGCrashlyticsKey);
#endif

#ifdef USE_FLURRY
    [Flurry setAppVersion:[[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleShortVersionString"]];
    [Flurry setSecureTransportEnabled:YES];
    [Flurry setShowErrorInLogEnabled:YES];
    [Flurry setLogLevel:FlurryLogLevelCriticalOnly];
    [Flurry startSession:TGFlurryApiKey];
    TGLogInfo(@"Flurry started with API Key %@ and for version %@", TGFlurryApiKey, [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleShortVersionString"]);
    TGLogInfo(@"Flurry Agent Version %@", [Flurry getFlurryAgentVersion]);
#endif

    TGLogInfo(@"Logging services started");
}
+ (void)stopLogging
{
    TGLogInfo(@"Shutting down logging services");
    [DDLog removeAllLoggers];
}

+ (NSString *)getUdidKey
{
    return [[UIDevice currentDevice] identifierForVendor].UUIDString;
}

+ (void)setUserID:(NSString *)userID
{
#ifdef USE_CRASHLYTICS
    [Crashlytics setUserIdentifier:userID];
#endif
}

+ (void)setUsername:(NSString *)username
{
#ifdef USE_CRASHLYTICS
    [Crashlytics setUserName:username];
#endif

#ifdef USE_FLURRY
    [Flurry setUserID:username];
#endif

}

+ (void)setUserEmail:(NSString *)email
{
#ifdef USE_CRASHLYTICS
    [Crashlytics setUserEmail:email];
#endif

}

+ (void)setUserAge:(NSUInteger)age
{
#ifdef USE_FLURRY
    [Flurry setAge:(int)age];
#endif

}

+ (void)setUserGender:(NSString *)gender
{
#ifdef USE_FLURRY
    [Flurry setGender:gender];
#endif
}

+ (void)setUserLocation:(CLLocation *)location
{
#ifdef USE_FLURRY
    [Flurry setLatitude:location.coordinate.latitude longitude:location.coordinate.longitude horizontalAccuracy:location.horizontalAccuracy verticalAccuracy:location.verticalAccuracy];
#endif
#ifdef USE_CRASHLYTICS
    [Crashlytics setObjectValue:location forKey:@"location"];
#endif
}

+ (void)setUserValue:(NSString *)value forKey:(NSString *)key
{
#ifdef USE_CRASHLYTICS
    [Crashlytics setObjectValue:value forKey:key];
#endif
}

#pragma mark - Report key/values with crash logs

+ (void)setIntValue:(int)value forKey:(NSString *)key
{
#ifdef USE_CRASHLYTICS
    [Crashlytics setIntValue:value forKey:key];
#endif
}

+ (void)setBoolValue:(BOOL)value forKey:(NSString *)key
{
#ifdef USE_CRASHLYTICS
    [Crashlytics setBoolValue:value forKey:key];
#endif
}

+ (void)setFloatValue:(float)value forKey:(NSString *)key
{
#ifdef USE_CRASHLYTICS
    [Crashlytics setFloatValue:value forKey:key];
#endif
}

void TGReportMilestone(NSString *milestone, NSDictionary *parameters)
{
    NSCParameterAssert(milestone);
    TGLogCInfo(@"Reporting %@", milestone);

#ifdef USE_FLURRY
    [Flurry logEvent:milestone withParameters:parameters];
#endif
}

void TGReportBeginTimedMilestone(NSString *milestone, NSDictionary *parameters)
{
    NSCParameterAssert(milestone);
    TGLogCInfo(@"Starting timed event %@", milestone);

#ifdef USE_FLURRY
    [Flurry logEvent:milestone withParameters:parameters timed:YES];
#endif
}

void TGReportEndTimedMilestone(NSString *milestone, NSDictionary *parameters)
{
    NSCParameterAssert(milestone);
    TGLogCInfo(@"Ending timed event %@", milestone);

#ifdef USE_FLURRY
    [Flurry endTimedEvent:milestone withParameters:parameters];
#endif
}


@end
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top