mirror of
https://github.com/copyrighttxt/watrbx-game-engine.git
synced 2026-09-04 20:57:49 +00:00
329 lines
11 KiB
Plaintext
329 lines
11 KiB
Plaintext
//
|
|
// ABTestManager.m
|
|
// RobloxMobile
|
|
//
|
|
// Created by Kyler Mulherin on 3/29/15.
|
|
// Copyright (c) 2015 ROBLOX. All rights reserved.
|
|
//
|
|
|
|
#import <UIKit/UIKit.h>
|
|
#import <Foundation/Foundation.h>
|
|
#import "ABTestManager.h"
|
|
#import "iOSSettingsService.h"
|
|
#import "RobloxWebUtility.h"
|
|
#import "RobloxInfo.h"
|
|
#import "RBXEventReporter.h"
|
|
#import "RBXFunctions.h"
|
|
|
|
#define ENROLLMENT_STATUS_ENROLLED @"Enrolled"
|
|
#define ENROLLMENT_STATUS_DECLINED @"Declined"
|
|
#define ENROLLMENT_STATUS_INACTIVE @"Inactive"
|
|
#define ENROLLMENT_STATUS_NO_EXPERIMENT @"NoExperiment"
|
|
|
|
// Experiment Names
|
|
#define EXPERIMENT_MOBILE_GUEST_MODE @"MobileGuestMode"
|
|
#define EXPERIMENT_GUEST_FLAVOR_TEXT @"GuestFlavorText"
|
|
|
|
// Enrollment Numbers
|
|
// Note - Most tests involve an A-A-B variation style
|
|
// so usually the behavior should be shown on variations of 2
|
|
DYNAMIC_FASTINTVARIABLE(EnrolledVariationMobileGuestMode, 2)
|
|
DYNAMIC_FASTINTVARIABLE(EnrolledVariationGuestFlavorText, 2)
|
|
|
|
@interface ABTestManager ()
|
|
|
|
@property (nonatomic, strong) NSMutableDictionary *enrollments;
|
|
@property (nonatomic, strong) NSString *browserTrackerId;
|
|
|
|
@property (nonatomic, strong) NSArray *experimentsBrowserTracker;
|
|
@property (nonatomic, strong) NSArray *experimentsUser;
|
|
|
|
@property (nonatomic) BOOL userInTestMobileGuestMode;
|
|
@property (nonatomic) BOOL userInTestGuestFlavorText;
|
|
|
|
@end
|
|
|
|
@implementation ABTestManager
|
|
|
|
+(id) sharedInstance
|
|
{
|
|
static dispatch_once_t rbxTutorialManPred = 0;
|
|
__strong static id _sharedObject = nil;
|
|
dispatch_once(&rbxTutorialManPred, ^{ // Need to use GCD for thread-safe allocation
|
|
_sharedObject = [[ABTestManager alloc] init];
|
|
});
|
|
return _sharedObject;
|
|
}
|
|
|
|
-(id) init
|
|
{
|
|
self = [super init];
|
|
if (self)
|
|
{
|
|
#ifdef ROBLOX_DEVELOPER
|
|
//declare all the ROBLOX DEVELOPER experiments
|
|
self.experimentsBrowserTracker = @[];
|
|
self.experimentsUser = @[];
|
|
|
|
#else
|
|
|
|
//declare all the ROBLOX MOBILE experiments
|
|
self.experimentsBrowserTracker = @[ EXPERIMENT_MOBILE_GUEST_MODE, EXPERIMENT_GUEST_FLAVOR_TEXT ];
|
|
self.experimentsUser = @[];
|
|
#endif
|
|
|
|
//initialize the experimental values
|
|
self.userInTestMobileGuestMode = NO;
|
|
self.userInTestGuestFlavorText = NO;
|
|
|
|
|
|
//fetch a browser tracker
|
|
self.browserTrackerId = [self getBrowserTracker];
|
|
self.enrollments = [NSMutableDictionary dictionary];
|
|
}
|
|
return self;
|
|
}
|
|
|
|
#pragma mark - Accessors
|
|
-(BOOL) IsPartOfTestMobileGuestMode { return [self isPartOfTest:EXPERIMENT_MOBILE_GUEST_MODE]; }
|
|
-(BOOL) IsPartOfTestGuestFlavorMode { return [self isPartOfTest:EXPERIMENT_GUEST_FLAVOR_TEXT]; }
|
|
|
|
-(BOOL) IsInTestMobileGuestMode { return self.userInTestMobileGuestMode; }
|
|
-(BOOL) IsInTestGuestFlavorMode { return self.userInTestGuestFlavorText; }
|
|
|
|
|
|
#pragma mark - Helper functions
|
|
|
|
-(NSString*) getBrowserTracker
|
|
{
|
|
//there is an old browser tracker stored in the defaults from legacy code, erase it (8/12/2015)
|
|
//TO DO - after a few releases, remove this code. Current Version : 207
|
|
|
|
if ([[NSUserDefaults standardUserDefaults] objectForKey:BROWSER_TRACKER_KEY_OLD])
|
|
{
|
|
[[NSUserDefaults standardUserDefaults] removeObjectForKey:BROWSER_TRACKER_KEY_OLD];
|
|
[[NSUserDefaults standardUserDefaults] synchronize];
|
|
}
|
|
|
|
NSString* bt = [RobloxData browserTrackerId];
|
|
if ([RBXFunctions isEmpty:bt])
|
|
[RobloxData initializeBrowserTrackerWithCompletion:^(bool success, NSString *browserTracker) {
|
|
if (success)
|
|
_browserTrackerId = browserTracker;
|
|
}];
|
|
|
|
return bt;
|
|
}
|
|
|
|
-(NSDictionary*) getExperiments:(NSArray*)experiments withTargetType:(ABTestSubjectType)subject withTargetId:(id)target
|
|
{
|
|
int expCount = experiments ? [experiments count] : 0;
|
|
if (expCount <= 0)
|
|
return @{};
|
|
|
|
//create the post data
|
|
int i = 0;
|
|
NSString* postData = @"{\"enrollments\":[";
|
|
while (i < expCount)
|
|
{
|
|
NSString* strExperiment = [NSString stringWithFormat:@"%@{\"ExperimentName\":\"%@\",\"SubjectType\":\"%li\",\"SubjectTargetId\":\"%@\"}",
|
|
(i > 0) ? @"," : @"",
|
|
experiments[i],
|
|
(long)subject,
|
|
target];
|
|
postData = [postData stringByAppendingString:strExperiment];
|
|
i++;
|
|
}
|
|
postData = [postData stringByAppendingString:@"]}"];
|
|
|
|
return [RobloxData enrollInABTests:postData];
|
|
}
|
|
|
|
-(NSDictionary*) getBrowserTrackerExperiments
|
|
{
|
|
if ([RBXFunctions isEmpty:self.browserTrackerId])
|
|
return @{};
|
|
|
|
return [self getExperiments:self.experimentsBrowserTracker withTargetType:ABTestSubjectTypeBrowserTracker withTargetId:self.browserTrackerId];
|
|
}
|
|
|
|
-(NSDictionary*) getUserExperiments:(NSNumber*)userId
|
|
{
|
|
if (!userId)
|
|
return @{};
|
|
|
|
return [self getExperiments:self.experimentsUser withTargetType:ABTestSubjectTypeUser withTargetId:userId];
|
|
}
|
|
|
|
|
|
-(void) fetchExperimentsForBrowserTracker
|
|
{
|
|
if (self.experimentsBrowserTracker.count <= 0)
|
|
return;
|
|
|
|
[self.enrollments setValuesForKeysWithDictionary:[self getBrowserTrackerExperiments]];
|
|
|
|
[self checkEnrollment];
|
|
}
|
|
|
|
-(void) fetchExperimentsForUser:(NSNumber*)userId
|
|
{
|
|
//this function gets called when the user logs in
|
|
if (self.experimentsUser.count <= 0 || userId == nil)
|
|
return;
|
|
|
|
//clear out any of of the old experiments from the last user
|
|
for (int i = 0; i < self.experimentsUser.count; i++)
|
|
if (self.enrollments[self.experimentsUser[i]])
|
|
[self.enrollments setNilValueForKey:self.experimentsUser[i]];
|
|
|
|
//add the experiments for the new user
|
|
[self.enrollments setValuesForKeysWithDictionary:[self getUserExperiments:userId]];
|
|
|
|
[self checkEnrollment];
|
|
}
|
|
|
|
#pragma mark - Tests
|
|
-(void) checkEnrollment
|
|
{
|
|
//call all checks here
|
|
[self checkTestMobileGuestMode];
|
|
[self checkTestGuestFlavorText];
|
|
|
|
|
|
//----------- DEBUG -------------
|
|
NSLog(@"");
|
|
NSLog(@"=== AB TEST ENROLLMENTS ===");
|
|
NSLog(@" - Mobile Guest Mode : %@", self.userInTestMobileGuestMode ? @"Enrolled" : @"Not Enrolled");
|
|
NSLog(@" - Guest Flavor Text : %@", self.userInTestGuestFlavorText ? @"Enrolled" : @"Not Enrolled");
|
|
NSLog(@"");
|
|
}
|
|
-(BOOL) isPartOfTest:(NSString*)testName
|
|
{
|
|
RBXABTest* test = (RBXABTest*)self.enrollments[testName];
|
|
if ([test isKindOfClass:[RBXABTest class]])
|
|
{
|
|
return ([test.status isEqualToString:ENROLLMENT_STATUS_ENROLLED]);
|
|
}
|
|
|
|
return NO;
|
|
}
|
|
-(BOOL) isEnrolled:(RBXABTest*)test inVariation:(int)variation
|
|
{
|
|
if (![RBXFunctions isEmpty:test] && [test isKindOfClass:[RBXABTest class]])
|
|
{
|
|
//first check if the test is active for everyone
|
|
if (test.isLockedOn)
|
|
return YES;
|
|
|
|
//Activate the AB Test behavior if the user is both enrolled in the test and in the correct test variation
|
|
BOOL isEnrolled = ([test.status isEqualToString:ENROLLMENT_STATUS_ENROLLED]);
|
|
BOOL isInVariation = (test.variation == variation);
|
|
|
|
return (isInVariation && isEnrolled);
|
|
}
|
|
|
|
return NO;
|
|
}
|
|
-(void) saveTestStatus:(RBXABTest*)test
|
|
{
|
|
// Store the returned test data to NSUserDefaults
|
|
NSData *data = [NSKeyedArchiver archivedDataWithRootObject:test];
|
|
[[NSUserDefaults standardUserDefaults] setObject:data forKey:[self keyForTestWithName:test.experimentName]];
|
|
[[NSUserDefaults standardUserDefaults] synchronize];
|
|
}
|
|
|
|
- (NSString *) keyForTestWithName:(NSString *)testName
|
|
{
|
|
NSString *experimentKey = [NSString stringWithFormat:@"%@_KEY", testName];
|
|
return experimentKey;
|
|
}
|
|
|
|
-(void) checkTestMobileGuestMode
|
|
{
|
|
//This AB Test is designed to remove the Welcome Screen.
|
|
//When the user starts the app or logs out, they are taken to the Games Page.
|
|
|
|
self.userInTestMobileGuestMode = NO;
|
|
|
|
//check if we have disabled the test from the server
|
|
iOSSettingsService* iOSSettings = [[RobloxWebUtility sharedInstance] getCachediOSSettings];
|
|
bool mobileGuestModeEnabled = iOSSettings->GetValueEnableABTestMobileGuestMode();
|
|
if (!mobileGuestModeEnabled) { return; }
|
|
|
|
//parse the result of the enrollment
|
|
if ([RBXFunctions isEmpty:self.enrollments[EXPERIMENT_MOBILE_GUEST_MODE]])
|
|
return;
|
|
|
|
RBXABTest *experiment = self.enrollments[EXPERIMENT_MOBILE_GUEST_MODE];
|
|
NSString *experimentName = nil;
|
|
if ([experiment isKindOfClass:[RBXABTest class]]) {
|
|
experimentName = experiment.experimentName;
|
|
}
|
|
|
|
if (![RBXFunctions isEmpty:[[NSUserDefaults standardUserDefaults] objectForKey:[self keyForTestWithName:experimentName]]]) {
|
|
RBXABTest *test = [NSKeyedUnarchiver unarchiveObjectWithData:[[NSUserDefaults standardUserDefaults] objectForKey:[self keyForTestWithName:experimentName]]];
|
|
self.userInTestMobileGuestMode = [self isEnrolled:test inVariation:DFInt::EnrolledVariationMobileGuestMode];
|
|
return;
|
|
}
|
|
|
|
self.userInTestMobileGuestMode = [self isEnrolled:self.enrollments[EXPERIMENT_MOBILE_GUEST_MODE] inVariation:DFInt::EnrolledVariationMobileGuestMode];
|
|
[self saveTestStatus:experiment];
|
|
}
|
|
|
|
-(void) checkTestGuestFlavorText
|
|
{
|
|
//This AB Test is designed to improve the Guest experience.
|
|
//When the user taps on a home
|
|
|
|
self.userInTestGuestFlavorText = NO; //YES FOR DEBUG
|
|
|
|
//check if we have disabled the test from the server
|
|
iOSSettingsService* iOSSettings = [[RobloxWebUtility sharedInstance] getCachediOSSettings];
|
|
bool guestFlavorTextEnabled = iOSSettings->GetValueEnableABTestGuestFlavorText();
|
|
if (!guestFlavorTextEnabled) { return; }
|
|
|
|
//parse the result of the enrollment
|
|
if ([RBXFunctions isEmpty:self.enrollments[EXPERIMENT_GUEST_FLAVOR_TEXT]])
|
|
return;
|
|
|
|
RBXABTest *experiment = self.enrollments[EXPERIMENT_GUEST_FLAVOR_TEXT];
|
|
NSString *experimentName = nil;
|
|
if ([experiment isKindOfClass:[RBXABTest class]]) {
|
|
experimentName = experiment.experimentName;
|
|
}
|
|
|
|
if (![RBXFunctions isEmpty:[[NSUserDefaults standardUserDefaults] objectForKey:[self keyForTestWithName:experimentName]]]) {
|
|
RBXABTest *test = [NSKeyedUnarchiver unarchiveObjectWithData:[[NSUserDefaults standardUserDefaults] objectForKey:[self keyForTestWithName:experimentName]]];
|
|
if ([test isKindOfClass:[RBXABTest class]]) {
|
|
if (test.isLockedOn) {
|
|
self.userInTestGuestFlavorText = YES;
|
|
return;
|
|
}
|
|
|
|
BOOL isEnrolled = ([test.status isEqualToString:ENROLLMENT_STATUS_ENROLLED]);
|
|
BOOL isInVariation = (test.variation == DFInt::EnrolledVariationGuestFlavorText);
|
|
|
|
BOOL enrollmentStatus = (isInVariation && isEnrolled);
|
|
self.userInTestGuestFlavorText = enrollmentStatus;
|
|
return;
|
|
}
|
|
}
|
|
|
|
self.userInTestGuestFlavorText = [self isEnrolled:self.enrollments[EXPERIMENT_GUEST_FLAVOR_TEXT] inVariation:DFInt::EnrolledVariationGuestFlavorText];
|
|
}
|
|
|
|
- (void) removeStoredExperiments
|
|
{
|
|
for (NSString *experimentName in self.experimentsBrowserTracker) {
|
|
RBXABTest *test = [NSKeyedUnarchiver unarchiveObjectWithData:[[NSUserDefaults standardUserDefaults] objectForKey:[self keyForTestWithName:experimentName]]];
|
|
if ([test isKindOfClass:[RBXABTest class]]) {
|
|
[[NSUserDefaults standardUserDefaults] removeObjectForKey:[self keyForTestWithName:experimentName]];
|
|
}
|
|
}
|
|
|
|
[[NSUserDefaults standardUserDefaults] synchronize];
|
|
}
|
|
|
|
@end
|