google/EarlGrey

View on GitHub
EarlGrey/Common/GREYAnalytics.m

Summary

Maintainability
Test Coverage
//
// Copyright 2016 Google Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//

#import "Common/GREYAnalytics.h"

#import <XCTest/XCTest.h>

#import "Additions/NSString+GREYAdditions.h"
#import "Additions/NSURL+GREYAdditions.h"
#import "Additions/XCTestCase+GREYAdditions.h"
#import "Common/GREYAnalyticsDelegate.h"
#import "Common/GREYConfiguration.h"
#import "Common/GREYLogger.h"

/**
 *  The Analytics tracking ID that receives EarlGrey usage data.
 */
static NSString *const kGREYAnalyticsTrackingID = @"UA-54227235-9";

/**
 *  The endpoint that receives EarlGrey usage data.
 */
static NSString *const kTrackingEndPoint = @"https://ssl.google-analytics.com/collect";

@interface GREYAnalytics() <GREYAnalyticsDelegate>
@end

@implementation GREYAnalytics {
  // Overridden GREYAnalytics delegate for custom handling of analytics.
  __weak id<GREYAnalyticsDelegate> _delegate;
  // Once set, analytics will be sent on next XCTestCase tearDown.
  BOOL _earlgreyWasCalledInXCTestContext;
}

+ (void)initialize {
  if (self == [GREYAnalytics class]) {
    NSString *analyticsRegEx = [NSString stringWithFormat:@".*%@.*", kGREYAnalyticsTrackingID];
    [NSURL grey_addBlacklistRegEx:analyticsRegEx];
  }
}

+ (instancetype)sharedInstance {
  static GREYAnalytics *sharedInstance = nil;
  static dispatch_once_t token = 0;
  dispatch_once(&token, ^{
    sharedInstance = [[GREYAnalytics alloc] initOnce];
  });
  return sharedInstance;
}

- (instancetype)initOnce {
  self = [super init];
  if (self) {
    _delegate = nil;
    _earlgreyWasCalledInXCTestContext = NO;
    // Register as an observer for kGREYXCTestCaseInstanceDidTearDown.
    [[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(grey_testCaseInstanceDidTearDown)
                                                 name:kGREYXCTestCaseInstanceDidTearDown
                                               object:nil];
  }
  return self;
}

- (void)didInvokeEarlGrey {
  // Track only if EarlGrey is called in the context of a test case.
  if ([XCTestCase grey_currentTestCase]) {
    _earlgreyWasCalledInXCTestContext = YES;
  }
}

- (void)setDelegate:(id<GREYAnalyticsDelegate>)delegate {
  _delegate = delegate;
}

- (id<GREYAnalyticsDelegate>)delegate {
  // The default delegate is self.
  return _delegate ? _delegate : self;
}

/** Custom NSURLSession which uses fixed user agent. */
+ (NSURLSession *)analyticsURLSession {
  static dispatch_once_t onceToken;
  static NSURLSession *session;
  dispatch_once(&onceToken, ^{
    NSURLSessionConfiguration *configuration =
        [NSURLSessionConfiguration ephemeralSessionConfiguration];
    configuration.HTTPAdditionalHeaders = @{ @"User-Agent": @"EarlGrey" };
    session = [NSURLSession sessionWithConfiguration:configuration];
  });
  return session;
}

+ (void)sendEventHitWithTrackingID:(NSString *)trackingID
                          clientID:(NSString *)clientID
                          category:(NSString *)category
                            action:(NSString *)action
                             value:(NSString *)value {
  // Initialize the payload with version(=1), tracking ID, client ID, category, action, and value.
  NSMutableString *payload = [[NSMutableString alloc] initWithFormat:@"v=1"
                                                                     @"&t=event"
                                                                     @"&tid=%@"
                                                                     @"&cid=%@"
                                                                     @"&ec=%@"
                                                                     @"&ea=%@"
                                                                     @"&ev=%@",
                                                                     trackingID,
                                                                     clientID,
                                                                     category,
                                                                     action,
                                                                     value];

  NSURLComponents *components = [[NSURLComponents alloc] initWithString:kTrackingEndPoint];
  [components setQuery:payload];
  NSURL *url = [components URL];

  [[[self analyticsURLSession] dataTaskWithURL:url
                             completionHandler:^(NSData *data,
                                                 NSURLResponse *response,
                                                 NSError *error) {
                               if (error) {
                                 // Failed to send analytics data, but since the test might be
                                 // running in a sandboxed environment it's not a good idea to
                                 // freeze or throw assertions, let's just log and move on.
                                 GREYLogVerbose(@"Failed to send analytics data due to: %@", error);
                               }
                             }] resume];
}

#pragma mark - GREYAnalyticsDelegate

- (void)trackEventWithTrackingID:(NSString *)trackingID
                        clientID:(NSString *)clientID
                        category:(NSString *)category
                          action:(NSString *)action
                           value:(NSString *)value {
  [GREYAnalytics sendEventHitWithTrackingID:trackingID
                                   clientID:clientID
                                   category:category
                                     action:action
                                      value:value];
}

#pragma mark - Private

/**
 *  Usage data is sent via Google Analytics indicating completion of a test case, if a delegate is
 *  specified it is invoked to handle the analytics instead.
 *  EarlGrey uses Google Analytics's event tracking with *anonymized* bundle ID (md5) as the
 *  category and "TestCase_{x}" as the sub-category where 'x' is the current test case count.
 */
- (void)grey_testCaseInstanceDidTearDown {
  if (_earlgreyWasCalledInXCTestContext) {
    // Reset var to track multiple test case invocations.
    _earlgreyWasCalledInXCTestContext = NO;

    if (GREY_CONFIG_BOOL(kGREYConfigKeyAnalyticsEnabled)) {
      NSString *bundleIDMD5 = [[[NSBundle mainBundle] bundleIdentifier] grey_md5String];
      if (!bundleIDMD5) {
        // If bundle ID is not available we use a placeholder.
        bundleIDMD5 = @"<Missing Bundle ID>";
      }
      XCTestCase *testCase = [XCTestCase grey_currentTestCase];
      NSString *testCaseMD5 =
          [[NSString stringWithFormat:@"%@::%@",
                                      [testCase grey_testClassName],
                                      [testCase grey_testMethodName]] grey_md5String];
      NSString *action = [NSString stringWithFormat:@"TestCase_%@", testCaseMD5];
      NSString *clientID = [[NSUUID UUID] UUIDString];
      NSUInteger testCaseCount = [[XCTestSuite defaultTestSuite] testCaseCount];
      NSString *value = [NSString stringWithFormat:@"%lu", (unsigned long)testCaseCount];
      [self.delegate trackEventWithTrackingID:kGREYAnalyticsTrackingID
                                     clientID:clientID
                                     category:bundleIDMD5
                                       action:action
                                        value:value];
    }
  }
}

@end