Proyectos de Subversion Iphone Microlearning

Rev

Autoría | Ultima modificación | Ver Log |

// Copyright 2019 Google
//
// 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 "FirebaseABTesting/Sources/Public/FirebaseABTesting/FIRExperimentController.h"

#import "FirebaseABTesting/Sources/ABTConditionalUserPropertyController.h"
#import "FirebaseABTesting/Sources/ABTConstants.h"
#import "FirebaseABTesting/Sources/Private/ABTExperimentPayload.h"
#import "FirebaseABTesting/Sources/Public/FirebaseABTesting/FIRLifecycleEvents.h"
#import "FirebaseCore/Sources/Private/FirebaseCoreInternal.h"

#import "Interop/Analytics/Public/FIRAnalyticsInterop.h"

/// Logger Service String.
FIRLoggerService kFIRLoggerABTesting = @"[Firebase/ABTesting]";

/// Default experiment overflow policy.
const ABTExperimentPayloadExperimentOverflowPolicy FIRDefaultExperimentOverflowPolicy =
    ABTExperimentPayloadExperimentOverflowPolicyDiscardOldest;

/// Deserialize the experiment payloads.
ABTExperimentPayload *ABTDeserializeExperimentPayload(NSData *payload) {
  // Verify that we have a JSON object.
  NSError *error;
  id JSONObject = [NSJSONSerialization JSONObjectWithData:payload options:kNilOptions error:&error];
  if (JSONObject == nil) {
    FIRLogError(kFIRLoggerABTesting, @"I-ABT000001", @"Failed to parse experiment payload: %@",
                error.debugDescription);
  }
  return [ABTExperimentPayload parseFromData:payload];
}

/// Returns a list of experiments to be set given the payloads and current list of experiments from
/// Firebase Analytics. If an experiment is in payloads but not in experiments, it should be set to
/// Firebase Analytics.
NSArray<ABTExperimentPayload *> *ABTExperimentsToSetFromPayloads(
    NSArray<NSData *> *payloads,
    NSArray<NSDictionary<NSString *, NSString *> *> *experiments,
    id<FIRAnalyticsInterop> _Nullable analytics) {
  NSArray<NSData *> *payloadsCopy = [payloads copy];
  NSArray *experimentsCopy = [experiments copy];
  NSMutableArray *experimentsToSet = [[NSMutableArray alloc] init];
  ABTConditionalUserPropertyController *controller =
      [ABTConditionalUserPropertyController sharedInstanceWithAnalytics:analytics];

  // Check if the experiment is in payloads but not in experiments.
  for (NSData *payload in payloadsCopy) {
    ABTExperimentPayload *experimentPayload = ABTDeserializeExperimentPayload(payload);
    if (!experimentPayload) {
      FIRLogInfo(kFIRLoggerABTesting, @"I-ABT000002",
                 @"Either payload is not set or it cannot be deserialized.");
      continue;
    }

    BOOL isExperimentSet = NO;
    for (id experiment in experimentsCopy) {
      if ([controller isExperiment:experiment theSameAsPayload:experimentPayload]) {
        isExperimentSet = YES;
        break;
      }
    }

    if (!isExperimentSet) {
      [experimentsToSet addObject:experimentPayload];
    }
  }
  return [experimentsToSet copy];
}

/// Returns a list of experiments to be cleared given the payloads and current list of
/// experiments from Firebase Analytics. If an experiment is in experiments but not in payloads, it
/// should be cleared in Firebase Analytics.
NSArray *ABTExperimentsToClearFromPayloads(
    NSArray<NSData *> *payloads,
    NSArray<NSDictionary<NSString *, NSString *> *> *experiments,
    id<FIRAnalyticsInterop> _Nullable analytics) {
  NSMutableArray *experimentsToClear = [[NSMutableArray alloc] init];
  ABTConditionalUserPropertyController *controller =
      [ABTConditionalUserPropertyController sharedInstanceWithAnalytics:analytics];

  // Check if the experiment is in experiments but not payloads.
  for (id experiment in experiments) {
    BOOL doesExperimentNoLongerExist = YES;
    for (NSData *payload in payloads) {
      ABTExperimentPayload *experimentPayload = ABTDeserializeExperimentPayload(payload);
      if (!experimentPayload) {
        FIRLogInfo(kFIRLoggerABTesting, @"I-ABT000002",
                   @"Either payload is not set or it cannot be deserialized.");
        continue;
      }

      if ([controller isExperiment:experiment theSameAsPayload:experimentPayload]) {
        doesExperimentNoLongerExist = NO;
      }
    }
    if (doesExperimentNoLongerExist) {
      [experimentsToClear addObject:experiment];
    }
  }
  return experimentsToClear;
}

// ABT doesn't provide any functionality to other components,
// so it provides a private, empty protocol that it conforms to and use it for registration.

@protocol FIRABTInstanceProvider
@end

@interface FIRExperimentController () <FIRABTInstanceProvider, FIRLibrary>
@property(nonatomic, readwrite, strong) id<FIRAnalyticsInterop> _Nullable analytics;
@end

@implementation FIRExperimentController

+ (void)load {
  [FIRApp registerInternalLibrary:(Class<FIRLibrary>)self withName:@"fire-abt"];
}

+ (nonnull NSArray<FIRComponent *> *)componentsToRegister {
  FIRDependency *analyticsDep = [FIRDependency dependencyWithProtocol:@protocol(FIRAnalyticsInterop)
                                                           isRequired:NO];
  FIRComponentCreationBlock creationBlock =
      ^id _Nullable(FIRComponentContainer *container, BOOL *isCacheable) {
    // Ensure it's cached so it returns the same instance every time ABTesting is called.
    *isCacheable = YES;
    id<FIRAnalyticsInterop> analytics = FIR_COMPONENT(FIRAnalyticsInterop, container);
    return [[FIRExperimentController alloc] initWithAnalytics:analytics];
  };
  FIRComponent *abtProvider = [FIRComponent componentWithProtocol:@protocol(FIRABTInstanceProvider)
                                              instantiationTiming:FIRInstantiationTimingLazy
                                                     dependencies:@[ analyticsDep ]
                                                    creationBlock:creationBlock];

  return @[ abtProvider ];
}

- (instancetype)initWithAnalytics:(nullable id<FIRAnalyticsInterop>)analytics {
  self = [super init];
  if (self != nil) {
    _analytics = analytics;
  }
  return self;
}

+ (FIRExperimentController *)sharedInstance {
  FIRApp *defaultApp = [FIRApp defaultApp];  // Missing configure will be logged here.
  id<FIRABTInstanceProvider> instance = FIR_COMPONENT(FIRABTInstanceProvider, defaultApp.container);

  // We know the instance coming from the container is a FIRExperimentController instance, cast it.
  return (FIRExperimentController *)instance;
}

- (void)updateExperimentsWithServiceOrigin:(NSString *)origin
                                    events:(FIRLifecycleEvents *)events
                                    policy:(ABTExperimentPayloadExperimentOverflowPolicy)policy
                             lastStartTime:(NSTimeInterval)lastStartTime
                                  payloads:(NSArray<NSData *> *)payloads
                         completionHandler:
                             (nullable void (^)(NSError *_Nullable error))completionHandler {
  FIRExperimentController *__weak weakSelf = self;
  dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
    FIRExperimentController *strongSelf = weakSelf;
    [strongSelf updateExperimentConditionalUserPropertiesWithServiceOrigin:origin
                                                                    events:events
                                                                    policy:policy
                                                             lastStartTime:lastStartTime
                                                                  payloads:payloads
                                                         completionHandler:completionHandler];
  });
}

- (void)
    updateExperimentConditionalUserPropertiesWithServiceOrigin:(NSString *)origin
                                                        events:(FIRLifecycleEvents *)events
                                                        policy:
                                                            (ABTExperimentPayloadExperimentOverflowPolicy)
                                                                policy
                                                 lastStartTime:(NSTimeInterval)lastStartTime
                                                      payloads:(NSArray<NSData *> *)payloads
                                             completionHandler:
                                                 (nullable void (^)(NSError *_Nullable error))
                                                     completionHandler {
  ABTConditionalUserPropertyController *controller =
      [ABTConditionalUserPropertyController sharedInstanceWithAnalytics:_analytics];

  // Get the list of expriments from Firebase Analytics.
  NSArray *experiments = [controller experimentsWithOrigin:origin];
  if (!experiments) {
    NSString *errorDescription =
        @"Failed to get conditional user properties from Firebase Analytics.";
    FIRLogInfo(kFIRLoggerABTesting, @"I-ABT000003", @"%@", errorDescription);

    if (completionHandler) {
      completionHandler([NSError
          errorWithDomain:kABTErrorDomain
                     code:kABTInternalErrorFailedToFetchConditionalUserProperties
                 userInfo:@{NSLocalizedDescriptionKey : errorDescription}]);
    }

    return;
  }
  NSArray<ABTExperimentPayload *> *experimentsToSet =
      ABTExperimentsToSetFromPayloads(payloads, experiments, _analytics);
  NSArray<NSDictionary<NSString *, NSString *> *> *experimentsToClear =
      ABTExperimentsToClearFromPayloads(payloads, experiments, _analytics);

  for (id experiment in experimentsToClear) {
    NSString *experimentID = [controller experimentIDOfExperiment:experiment];
    NSString *variantID = [controller variantIDOfExperiment:experiment];
    [controller clearExperiment:experimentID
                      variantID:variantID
                     withOrigin:origin
                        payload:nil
                         events:events];
  }

  for (ABTExperimentPayload *experimentPayload in experimentsToSet) {
    if (experimentPayload.experimentStartTimeMillis > lastStartTime * ABT_MSEC_PER_SEC) {
      [controller setExperimentWithOrigin:origin
                                  payload:experimentPayload
                                   events:events
                                   policy:policy];
      FIRLogInfo(kFIRLoggerABTesting, @"I-ABT000008",
                 @"Set Experiment ID %@, variant ID %@ to Firebase Analytics.",
                 experimentPayload.experimentId, experimentPayload.variantId);

    } else {
      FIRLogInfo(kFIRLoggerABTesting, @"I-ABT000009",
                 @"Not setting experiment ID %@, variant ID %@ due to the last update time %lld.",
                 experimentPayload.experimentId, experimentPayload.variantId,
                 (long)lastStartTime * ABT_MSEC_PER_SEC);
    }
  }

  if (completionHandler) {
    completionHandler(nil);
  }
}

- (NSTimeInterval)latestExperimentStartTimestampBetweenTimestamp:(NSTimeInterval)timestamp
                                                     andPayloads:(NSArray<NSData *> *)payloads {
  for (NSData *payload in [payloads copy]) {
    ABTExperimentPayload *experimentPayload = ABTDeserializeExperimentPayload(payload);
    if (!experimentPayload) {
      FIRLogInfo(kFIRLoggerABTesting, @"I-ABT000002",
                 @"Either payload is not set or it cannot be deserialized.");
      continue;
    }
    if (experimentPayload.experimentStartTimeMillis > timestamp * ABT_MSEC_PER_SEC) {
      timestamp = (double)(experimentPayload.experimentStartTimeMillis / ABT_MSEC_PER_SEC);
    }
  }
  return timestamp;
}

- (void)validateRunningExperimentsForServiceOrigin:(NSString *)origin
                         runningExperimentPayloads:(NSArray<ABTExperimentPayload *> *)payloads {
  ABTConditionalUserPropertyController *controller =
      [ABTConditionalUserPropertyController sharedInstanceWithAnalytics:_analytics];

  FIRLifecycleEvents *lifecycleEvents = [[FIRLifecycleEvents alloc] init];

  // Get the list of experiments from Firebase Analytics.
  NSArray<NSDictionary<NSString *, NSString *> *> *activeExperiments =
      [controller experimentsWithOrigin:origin];

  NSMutableSet *runningExperimentIDs = [NSMutableSet setWithCapacity:payloads.count];
  for (ABTExperimentPayload *payload in payloads) {
    [runningExperimentIDs addObject:payload.experimentId];
  }

  for (NSDictionary<NSString *, NSString *> *activeExperimentDictionary in activeExperiments) {
    NSString *experimentID = activeExperimentDictionary[@"name"];
    if (![runningExperimentIDs containsObject:experimentID]) {
      NSString *variantID = activeExperimentDictionary[@"value"];

      [controller clearExperiment:experimentID
                        variantID:variantID
                       withOrigin:origin
                          payload:nil
                           events:lifecycleEvents];
    }
  }
}

- (void)activateExperiment:(ABTExperimentPayload *)experimentPayload
          forServiceOrigin:(NSString *)origin {
  ABTConditionalUserPropertyController *controller =
      [ABTConditionalUserPropertyController sharedInstanceWithAnalytics:_analytics];

  FIRLifecycleEvents *lifecycleEvents = [[FIRLifecycleEvents alloc] init];

  // Ensure that trigger event is nil, which will immediately set the experiment to active.
  [experimentPayload clearTriggerEvent];

  [controller setExperimentWithOrigin:origin
                              payload:experimentPayload
                               events:lifecycleEvents
                               policy:experimentPayload.overflowPolicy];
}

@end