Custom Network Analytics for iOS Apps

Overview

In the 7.0 release of MobileFirst Platform Foundation, the WLAnalytics API for native iOS has been expanded to include 2 new utility methods: generateNetworkRequestMetadataWithURL: and generateNetworkResponseMetadataWithResponseData:andTrackingId:. These methods can be used in conjunction with networking APIs to record request metadata and send it to the Analytics server. The advantage of doing so is to have analytics recorded for any network events, even to non-MobileFirst services, while still retaining full control over the implementation of the requests and responses.

Implementing NSURLProtocol

In this example, I will show how to accomplish this using NSURLProtocol to intercept the request and NSURLConnectionDataDelegate to retrieve the response.

The goal of this app is simple: send a URL request to Google Maps and record the response. To start, let's make a button that creates an NSURLRequest and sends it via NSURLConnection.

1
2
3
4
- (IBAction)pingGoogleMaps:(UIButton *)sender {
    NSURLRequest* myRequest = [NSURLRequest requestWithURL:[NSURL URLWithString:@"https://www.google.com/maps"]];
    [NSURLConnection sendAsynchronousRequest:myRequest queue:[NSOperationQueue currentQueue] completionHandler:nil];
}

To intercept the request and its response, we will subclass NSURLProtocol,

1
2
3
#import <Foundation/Foundation.h>
@interface MyURLProtocol : NSURLProtocol
@end
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#import "MyURLProtocol.h"
#import "WLAnalytics.h"
#import "OCLogger.h"
@interface MyURLProtocol() <NSURLConnectionDataDelegate>
@property NSURLConnection* connection;
@end
@implementation MyURLProtocol
#pragma mark NSURLProtocol
+ (BOOL) canInitWithRequest:(NSURLRequest *)request {
    NSString* allowedURL = @"https://www.google.com/maps";
    if ( [request.URL.absoluteString isEqualToString:allowedURL] ) {
        if ( ! [NSURLProtocol propertyForKey:@"MyURLProtocolHandledKey" inRequest:request]) {
            return YES;
        }
    }
    return NO;
}
+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request {
    return request;
}
- (void)startLoading {
    NSMutableURLRequest *newRequest = [self.request mutableCopy];
    [NSURLProtocol setProperty:@YES forKey:@"MyURLProtocolHandledKey" inRequest:newRequest];
    self.connection = [NSURLConnection connectionWithRequest:newRequest delegate:self];
}
- (void)stopLoading {
    [self.connection cancel];
    self.connection = nil;
}

and implement the NSURLConnectionDataDelegate protocol.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#pragma mark NSURLConnectionDataDelegate
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response {
    [self.client URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageNotAllowed];
}
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
    [self.client URLProtocol:self didLoadData:data];
}
- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
    [self.client URLProtocolDidFinishLoading:self];
}
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error {
    [self.client URLProtocol:self didFailWithError:error];
}
@end
NSURLProtocol, see this tutorial.

Recording Analytics

In the startLoading method of MyURLProtocol.m, we will generate analytics metadata (which is needed for the data to appear in the Analytics console) and log the request.

1
2
3
4
5
6
7
8
- (void)startLoading {
    NSDictionary* analyticsRequestMetadata = [[WLAnalytics sharedInstance] generateNetworkRequestMetadataWithURL:@"https://www.google.com/maps"];
    self.analyticsTrackingId = analyticsRequestMetadata[@"$trackingid"];
    [[WLAnalytics sharedInstance] log:@"Google Maps network request" withMetadata:analyticsRequestMetadata];
    NSMutableURLRequest *newRequest = [self.request mutableCopy];
    [NSURLProtocol setProperty:@YES forKey:@"MyURLProtocolHandledKey" inRequest:newRequest];
    self.connection = [NSURLConnection connectionWithRequest:newRequest delegate:self];
}

We need the analyticsTrackingId property to hold the tracking ID, which is used to map the request to the response.

Next, we do the same for the response in the connection:didReceiveData: method.

1
2
3
4
5
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
    NSDictionary* analyticsResponseMetadata = [[WLAnalytics sharedInstance] generateNetworkResponseMetadataWithResponseData:data andTrackingId:self.analyticsTrackingId];
    [[WLAnalytics sharedInstance] log:@"Google Maps network response" withMetadata:analyticsResponseMetadata];
    [self.client URLProtocol:self didLoadData:data];
}

Finally, we need to send the analytics data to the server when the request is complete.

1
2
3
4
5
- (void)stopLoading {
    [[WLAnalytics sharedInstance] send];
    [self.connection cancel];
    self.connection = nil;
}

That concludes our setup of the NSURLProtocol. Now we have one remaining step: to register that class in the AppDelegate. This is necessary to ensure that all URL requests are handled by the MyURLProtocol class, no matter where in the app they are sent from.

1
2
3
4
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    [NSURLProtocol registerClass:[MyURLProtocol class]];
    return YES;
}

Here is a sample app that implements all of the above. To run it, create a native iOS environment in MobileFirst Platform Studio, and copy the worklight.plist file to the Xcode project: CustomNetworkAnalytics_iOS_app

Viewing the Results

The test app is now complete! When the app is run and the request sent, the message "Analytics data successfully sent to server" should appear at the bottom of the Xcode console, provided everything is set up properly. Now the request data should appear in the MobileFirst Operational Analytics console. If we navigate to the Network tab and go to Other Requests, we can see the average roundtrip time, average data size, and number of requests to Google Maps.

missing_alt
Last modified on May 01, 2016
Share this post: