seeking the truth from mobile analytics
TRANSCRIPT
PAGE VIEW COUNTING<script> (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){ (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o), m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m) })(window,document,'script','//www.google-analytics.com/analytics.js','ga');
ga('create', 'UA-12345678-9', 'auto'); ga('send', 'pageview');</script>
TOOLBOXNSDictionary *parameters = @{@"Source": @"List"};[analyticsProvider logEvent:@"Premium feature button tapped" parameters:parameters];
The purpose of an experiment is to find the truth.
Not to prove that you are right.— Joel Marsh (UX Architect)
WELL DESIGNED ANALYTICS1. Outline business and UX goals
2. Decide the questions you want to answer3. Map the events to answer your questions
4. Build user paths and conversion funnels from collected data
1. Outline business and UX goalsIncrease premium features selling
2. Decide the questions you want to answerIs the "Gallery" label an efficient selling driver ?
3.Map the events to answer your questions
Gallery button tap (List|Grid|Large)Gallery premium feature invite refusal
Gallery premium feature invite acceptancePremium feature button tap (List|Detail)
Premium feature buying intent (x€)Funnel abort (No Ads|User not logged)
The only way an experiment can fail is if the result teaches you nothing.
— Joel Marsh (UX Architect)
@interface SBTAnalyticsEvent : NSObject
@property (nonatomic, copy, readonly) NSString *name;
@property (nonatomic, copy, readonly) NSDictionary *parameters;
- (instancetype)initWithName:(NSString *)name parameters:(NSDictionary *)parameters;
//...@end
NSDictionary *parameters = @{SBTEventParameterLayoutKey: SBTEventParameterLayoutGrid};SBTAnalyticsEvent *event = [[SBTAnalyticsEvent alloc] initWithName:SBTEventGalleryButtonTap parameters:parameters];
@interface SBTAnalyticsTracker : NSObject
//...
- (void)trackEvent:(SBTAnalyticsEvent *)event;
//...
@end
@implementation SBTAnalyticsTracker
//...
- (void)trackEvent:(SBTAnalyticsEvent *)event{ NSCParameterAssert(event);#ifndef DEBUG [Flurry logEvent:event.name withParameters:event.parameters];#endif}
//...
@end
TAXILOG()Xcode colors plugin
C Macro#define XCODE_COLORS_ESCAPE @"\033["
// Clear any background and foreground color#define XCODE_COLORS_RESET XCODE_COLORS_ESCAPE @";" #define TAXI_COLOR_ESCAPE \ XCODE_COLORS_ESCAPE @"fg0,0,0;"\ XCODE_COLORS_ESCAPE @"bg255,204,0;"#ifdef DEBUG #define TAXILog(fmt, ...)\ NSLog((TAXI_COLOR_ESCAPE fmt XCODE_COLORS_RESET), ##__VA_ARGS__);#else #define TAXILog(...)#endif
BONJOURLOG()NSLogger
NSLogger Mac clientC Macro
#ifdef DEBUG #define LOGGER_DEFAULT_OPTIONS(kLoggerOption_BufferLogsUntilConnection | \ kLoggerOption_BrowseBonjour | \ kLoggerOption_BrowseOnlyLocalDomain | \ kLoggerOption_UseSSL) #define BONJOURLog(fmt, ...) \ LogMessageTo(LoggerGetDefaultLogger(), @"Analytics", 0, fmt, ##__VA_ARGS__);#else #define BONJOURLog(...)#endif
- (void)trackEvent:(SBTAnalyticsEvent *)event{ NSCParameterAssert(event);#ifndef DEBUG [Flurry logEvent:event.name withParameters:event.parameters];#endif#ifdef DEBUG NSString *log = [self logStringForTrackedEvent:event]; TAXILog(@"%@", log); BONJOURLog(@"%@", log);#endif}
INTEGRATION TESTING WITH KIF[tester tapViewWithAccessibilityLabel:@"Gallery"];[tester waitForViewWithAccessibilityLabel:@"This ad is featured"];[tester tapViewWithAccessibilityLabel:@"No, continue"];
TRACKING EXTENSION FOR KIFNSDictionary *parameters = @{@"Layout": @"Grid"};SBTAnalyticsEvent *event = [[SBTAnalyticsEvent alloc] initWithName:@"Gallery button tap" parameters:@{@"Layout": @"Grid"}];[tester tapViewWithAccessibilityLabel:@"Gallery"];[tester checkTrackedAnalyticsEvent:event];
TRACKING EXTENSION FOR KIF1. Proxy events to a stack
2. Intercept events before test suite begins3. Answer if an event is in the stack with YES or NO
4. Transform the boolean answer in a test result
1. Proxy events to a stack
@protocol SBTAnalyticsTrackerDelegate <NSObject>
- (void)analyticsTracker:(SBTAnalyticsTracker *)tracker didTrackEvent:(SBTAnalyticsEvent *)event;
@end
@interface SBTAnalyticsTracker : NSObject
@property (nonatomic, weak) id<SBTAnalyticsTrackerDelegate> delegate;
- (void)trackEvent:(SBTAnalyticsEvent *)event;
//...
@end
- (void)trackEvent:(SBTAnalyticsEvent *)event{ NSCParameterAssert(event);#ifndef DEBUG [Flurry logEvent:event.name withParameters:event.parameters];#endif [self.delegate analyticsTracker:self didTrackEvent:event];#ifdef DEBUG NSString *log = [self logStringForTrackedEvent:event]; TAXILog(@"%@", log); BONJOURLog(@"%@", log);#endif}
2.Intercept events before test suite begins
CONFIG_START
static dispatch_once_t onceToken;dispatch_once(&onceToken, ^{ SBTAnalyticsEventsStack *stack = [SBTAnalyticsEventsStack sharedAnalyticsEventsStack]; [[SBTAnalyticsTracker sharedTracker] setDelegate:stack];});
CONFIG_END
3.Answer if an event is in the stack with YES or NO
- (BOOL)popAnalyticsEvent:(SBTAnalyticsEvent *)event{ __block SBTAnalyticsEvent *expectedEvent; [self.events enumerateObjectsWithOptions:NSEnumerationReverse usingBlock: ^(SBTAnalyticsEvent *stackEvent, NSUInteger idx, BOOL *stop) { if ([stackEvent isEqualToEvent:event]) { expectedEvent = stackEvent; *stop = YES; } }]; if (!expectedEvent) { return NO; } [self.events removeObject:expectedEvent]; return YES;}
4.Transform the boolean answer in a test result@implementation KIFUITestActor (SBTAnalyticsTracking)
- (void)checkTrackedAnalyticsEvent:(SBTAnalyticsEvent *)event{ [self runBlock:^KIFTestStepResult(NSError *__autoreleasing *error) { SBTAnalyticsEventsStack *stack = [SBTAnalyticsEventsStack sharedAnalyticsEventsStack]; BOOL eventWasTracked = [stack popAnalyticsEvent:event]; if (eventWasTracked) { return KIFTestStepResultSuccess; } *error = [NSError KIFErrorWithFormat:@"Event not tracked %@", event]; return KIFTestStepResultFailure; } timeout:1.0];}
/...
@end
THANKSMouhcine El Amine - @[email protected]