The Full Stack Developer Guide Developer Hub

Welcome to the Full Stack Developer Guide developer hub. You'll find comprehensive guides and documentation to help you start working with the Full Stack Developer Guide as quickly as possible, as well as support if you get stuck. Let's jump right in!

Get Started    

Implement a User Profile Service

Use a User Profile Service to persist information about your users and ensure variation assignments are sticky. For example, if you are working on a backend website, you can create an implementation that reads and saves user profiles from a Redis or memcached store.

Non-mobile SDKs

In the non-mobile SDKs , there is no default implementation. Implementing a User Profile Service is optional and is only necessary if you want to keep variation assignments sticky even when experiment conditions are changed while it is running (for example, audiences, attributes, variation pausing, and traffic distribution). Otherwise, the non-mobile SDKs are stateless and rely on deterministic bucketing to return consistent assignments. See How bucketing works in Full Stack for more information.

Mobile SDKs

On mobile, the SDK defaults to a User Profile Service that stores this state directly on the device. See the Android SDK User Profile Service and the Swift SDK User Profile Service.

For both Android and Swift, use manager.userProfileService.lookup to read a customer’s user profile.

No op in Android

In Android, the default User Profile Service is also sticky. This can be overruled by disabling UserProfileService in your Android app. To do so, define a CustomUserProfileService class inheriting on our DefaultUserProfileService class, but overriding the lookupmethod to always return null. See the example below:

public class CustomUserProfileService extends DefaultUserProfileService {
 @Override
 public Map < String, Object > lookup(String userId) {
  return null;
 }
}

Pass this to your OptimizelyManager:

CustomUserProfileService customUserProfileService = new CustomUserProfileService();
optimizelyManager = OptimizelyManager.builder(PROJECT_ID).withUserProfileService(customUserProfileService).build(getApplicationContext());

With this custom implementation, the bucketing will no longer be sticky when the traffic allocation changes.

Implement a service

Refer to the code samples below to provide your own User Profile Service. It should expose two functions with the following signatures:

  • lookup: Takes a user ID string and returns a user profile matching the schema below.
  • save: Takes a user profile and persists it.

If you want to use the User Profile Service purely for tracking purposes and not sticky bucketing, you can implement only the save method (always return nil from lookup).

using System.Collections.Generic;

using OptimizelySDK;
using OptimizelySDK.Bucketing;

class InMemoryUserProfileService : UserProfileService
{
  private Dictionary<String, Dictionary<string, object>> userProfiles = new Dictionary<String, Dictionary<string, object>>();
  Dictionary<string, object> UserProfileService.Lookup(string userId)
  {
    // Retrieve and return user profile
    // Replace with userprofile variable
    return null;
  }

  void UserProfileService.Save(Dictionary<string, object> userProfile)
  {
    // Save user profile
  }
}

	var optimizelyClient = new Optimizely(
		datafile: datafile,
    userProfileService: userProfileService);

import com.optimizely.ab.bucketing.UserProfileService;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/** 
 * You can optionally provide an override to the default user profile service
 * below is an example override that is very thread safe but not persistent.
 */
UserProfileService userProfileService = new  UserProfileService() {
    ConcurrentHashMap<String, ConcurrentHashMap<String, Object>> userMap = new ConcurrentHashMap<>();

    @Override
    public Map<String, Object> lookup(String userId) throws Exception {
        return userMap.get(userId);
    }

    @Override
    public void save(Map<String, Object> userProfile) throws Exception {
        String userId = (String) userProfile.get("user_id");
        if (userId != null) {
            ConcurrentHashMap<String, Object> concurrentInnerMap = new ConcurrentHashMap<>(userProfile);
            userMap.put(userId, concurrentInnerMap);
        }
    }
};


Optimizely optimizelyClient = Optimizely.builder(datafile, eventHandler)
    .withUserProfileService(userProfileService)
    .build();

// Sample user profile service implementation
const userProfileService = {
  lookup: userId => {
    // Perform user profile lookup
  },
  save: userProfileMap => {
    // Persist user profile
  },
};

var optimizelyClient = optimizely.createInstance({
  datafile,
  userProfileService,
});

// Sample User Profile Service implementation
const userProfileService = {
  lookup: userId => {
    // Perform user profile lookup
  },
  save: userProfileMap => {
    // Persist user profile
  }
};

var optimizelyClientWithUPS = optimizely.createInstance({
  datafile,
  userProfileService
});

@implementation CustomUserProfileService

- (nonnull instancetype)init {
    self = [super init];
    if (self != nil) {
        return self;
    }
    return nil;
}
- (NSDictionary<NSString *, id> * _Nullable)lookupWithUserId:(NSString * _Nonnull)userId {
    // Retrieve and return user profile
    // Replace with userprofile variable
    
    return nil;
}
- (void)saveWithUserProfile:(NSDictionary<NSString *, id> * _Nonnull)userProfile {
    // save user profile
}

@end

  
// create OptimizelyClient with a custom Logger
CustomUserProfileService *userProfileService = [[CustomUserProfileService alloc] init];
     
self.optimizely = [[OptimizelyClient alloc] initWithSdkKey:kOptimizelySdkKey
                            				logger:nil
                            				eventDispatcher:nil
                            				userProfileService:userProfileService
                            				periodicDownloadInterval:5*60
                            				defaultLogLevel:OptimizelyLogLevelInfo];

use Optimizely\Logger\DefaultLogger;
use Optimizely\UserProfile\UserProfileServiceInterface;
use Optimizely\Optimizely;

class UserProfileService implements UserProfileServiceInterface
{
  public function lookup($userId)
  {
    // perform user profile lookup
  }

  public function save($userProfileMap) {
    // persist user profile
  }
}

$optimizelyClient = new Optimizely(
  $datafile,
  null,
  new DefaultLogger(),
  null,
  false,
  new UserProfileService()
);

from optimizely import user_profile
from optimizely import optimizely

class MyUserProfileService(user_profile.UserProfileService):
    def lookup(self, user_id):
        pass
        # Retrieve and return user profile

    def save(self, user_profile):
        pass
        # Save user profile

optimizely_client = optimizely.Optimizely(datafile, user_profile_service=MyUserProfileService())

# Sample user profile service implementation
class UserProfileService
  def lookup(user_id)
    # retrieve user profile
  end

  def save(user_profile)
    # save user profile
  end
end

optimizely_client = Optimizely::Project.new(datafile,
                                            Optimizely::EventDispatcher.new,
                                            Optimizely::NoOpLogger.new,
                                            nil,
                                            false,
                                            UserProfileService.new)

class CustomUserProfileService: OPTUserProfileService {
    func lookup(userId: String) -> UPProfile? {
        
        // Retrieve and return user profile
        // Replace with userprofile variable
        
        return nil
    }
    
    open func save(userProfile: UPProfile) {
        
        // save user profile
    }
}

let userProfileService = DefaultUserProfileService()

let optimizely = OptimizelyClient(sdkKey: sdkKey,
                              userProfileService: userProfileService)

The code example below shows the JSON schema for the user profile object.

Use experiment_bucket_map to override the default bucketing behavior and define an alternate experiment variation for a given user. For each experiment that you want to override, add an object to the map. Use the experiment ID as the key and include a variation_id property that specifies the desired variation. If there isn't an entry for an experiment, then the default bucketing behavior persists.

In the example below, ^[a-zA-Z0-9]+$ is the experiment ID.

{
  "title": "UserProfile",
  "type": "object",
  "properties": {
    "user_id": {"type": "string"},
    "experiment_bucket_map": {"type": "object",
                              "patternProperties": {
                                 "^[a-zA-Z0-9]+$": {"type": "object",
                                                    "properties": {"variation_id": {"type":"string"}},
                                                    "required": ["variation_id"]}
                               }
                             }
  },
  "required": ["user_id", "experiment_bucket_map"]
}

The SDK uses the User Profile Service you provide to override Optimizely's default bucketing behavior in cases when an experiment assignment has been saved.

For both Swift and Android apps, the User Profile Service will persist variation assignments across app updates. However, the User Profile Service will not persist variation assignments across app re-installs.

When implementing your own User Profile Service, we recommend loading the user profiles into the User Profile Service on initialization and avoiding performing expensive, blocking lookups on the lookup function to minimize the performance impact of incorporating the service.

When implementing in a multi-server or stateless environment, we suggest using this interface with a backend like Cassandra or Redis. You can decide how long you want to keep your sticky bucketing around by configuring these services.

Implement an asynchronous service in Javascript SDK 3.0

In the JavaScript SDK 3.0, you can implement attributes.$opt_experiment_bucket_map to perform asynchronous lookups of users' previous variations. The SDK handles attributes.$opt_experiment_bucket_map the same way it would userProfileService.lookup, and this allows you to do an asynchronous lookup of the experiment bucket map before passing it to the Activate method.

Note

  • attributes.$opt_experiment_bucket_map will always take precedence over an implemented userProfileService.lookup.
  • Because the Javascript SDK is stateless, you must use these attributes anywhere that you call the Activate, Get Variation, or Track methods.

The example below shows how to implement consistent bucketing via attributes.

const userId = 'user1'
// This would come from a DB call
const experimentBucketMap = {
  '123': { // experimentId
    'variation_id': '456', // the variationId
  }
}
const attributes = {
  '$opt_experiment_bucket_map': experimentBucketMap
}
/* The user will always get bucketed into variationid='456' for experiment id='123 */
const result = client.activate("my-experiment", userId, attributes)

You can use the asynchronous service example below to try this functionality in a test environment. If you implement this example in a production environment, be sure to modify UserProfileDB to the correct database.

const optimizely = require('@optimizely/optimizely-sdk')

// This is here only as an example; in a production environment this would be redis or some distributed database
class UserProfileDB {
  constructor() {
    /* Example structure
     * {
     *   user1: {
     *     user_id: 'user1',
     *     experiment_bucket_map: {
     *       '12095834311': { // experimentId
     *         variation_id: '12117244349' // variationId
     *       }
     *     }
     *   }
     * }
     */
    this.db = {}
  }

  async save(user_id, experiment_bucket_map) {
    return new Promise((resolve, reject) => {
      // Use setTimeout to simulate async
      setTimeout(() => {
        this.db[user_id] = { user_id, experiment_bucket_map }
        resolve()
      }, 50)
    })
  }

  async lookup(userId) {
    return new Promise((resolve, reject) => {
      // Use setTimeout to simulate async
      setTimeout(() => {
        let result
        if (this.db[userId] && this.db[userId].experiment_bucket_map) {
          result = this.db[userId].experiment_bucket_map
        }
        resolve(result)
      }, 50)
    })
  }
}

const userDb = new UserProfileDB()

const userProfileService = {
  lookup(userId) {
    // Lookup must be synchronous, so in our case we will not implement this function
  },
  save(userProfileMap) {
    const { user_id, experiment_bucket_map } = userProfileMap
    userDb.save(user_id, experiment_bucket_map)
  }
}

const client = optimizely.createInstance({
  datafile,
  userProfileService,
})

const userId = 'user1'
// Lookup the users experiment_bucket_map
const experimentBucketMap = await userDb.lookup(userId) || {}
const attributes = {
  $opt_experiment_bucket_map: experimentBucketMap
}

const result = client.activate("exp1", userId, attributes)
console.log('got variation', result) 


Implement a User Profile Service


Suggested Edits are limited on API Reference Pages

You can only suggest edits to Markdown body content, but not to the API spec.