Implement a user profile service

Use a User Profile Service to persist information about your users and ensure variation assignments are sticky. Sticky implies that once a user gets a particular variation, their assignment won't change.

In the React Native SDK, 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 React SDK is stateless and relies on deterministic bucketing to return consistent assignments.

If the User Profile Service doesn't bucket a user as you expect, then check whether other features are overriding the bucketing. For more information, see How bucketing works.

Implement a User Profile 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).

import { createInstance } from '@optimizely/react-sdk';

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

const optimizelyClient = createInstance({
  datafile: optimizelyDatafile, // assuming you have a hardcoded datafile
  userProfileService, // Passing your userProfileService created above
});

The following code example 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 React SDK uses the User Profile Service you provide to override Optimizely's default bucketing behavior in cases when an experiment assignment has been saved.

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.

Implement a user profile service using React Native Async Storage

Refer to the following code samples to provide your own user profile service using React Native async storage.

Import the React SDK and React Native async storage packages.

import { createInstance } from '@optimizely/react-sdk';
import AsyncStorage from '@react-native-community/async-storage';

Create a user profile service object.

const userProfileService = {
  lookup: (userId) => {
    // Keeping lookup empty because we are using an async storage implementation
  },
  save: userProfileMap => {
    const {
      user_id: userId,
      experiment_bucket_map: experimentBucketMap,
    } = userProfileMap;
    AsyncStorage.setItem(
      'optly-user-profiles-' + userId,
      JSON.stringify(experimentBucketMap)
    ).then(() => console.log('User profile saved successfully'))
     .catch(err => console.log('Failed to save user profile', err));
  },
};

React Native async storage is asynchronous. This means you need to implement a custom lookup to get user’s experiment bucket map and then pass it on as an attribute. Refer to the following code sample to implement a custom lookup.

// look up the user's experiment_bucket_map
const customAsyncLookup = async (userId) => {
  const experimentBucketMap = await AsyncStorage.getItem('optly-user-profiles-' + userId);
  return !!experimentBucketMap ? JSON.parse(experimentBucketMap) : {};
};

Create an Optimizely client and pass userProfileService to it.

const optimizelyClientInstance = createInstance({ datafile, userProfileService });

Get the experiment bucket map for the user using custom lookup and then pass it on as an attribute to OptimizelyProvider component.

const user = {
  id: userId,
  attributes: {
    $opt_experiment_bucket_map: await customAsyncLookup(userId)
  }
}

class App extends React.Component {
  render() {
    return (
      <OptimizelyProvider
        optimizely={optimizelyClientInstance}
        user={user}
      >
        {/* … your application components here … */}
      </OptimizelyProvider>
    );
  }
}

Implement asynchronous user lookups with experiment bucket map attribute

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 OptimizelyProvider component.

📘

Note

attributes.$opt_experiment_bucket_map will always take precedence over an implemented userProfileService.lookup.

The following example shows how to implement consistent bucketing from a user profile service using the reserved $opt_experiment_bucket_map attribute.

import React from 'react';
import {
  createInstance,
  OptimizelyProvider,
} from '@optimizely/react-sdk'

const optimizelyClient = createInstance({
  datafile: optimizelyDatafile, // assuming you have a harcoded datafile
});

// In practice, this could come from a DB call
const experimentBucketMap = {
  123: { // ID of experiment
    variation_id: '456', // ID of variation to force for this experiment
  }
}

const user = {
  id: ‘myuser123’,
  attributes: {
    // By passing this $opt_experiment_bucket_map, we force that the user
    // will always get bucketed into variationid='456' for experiment id='123'
    '$opt_experiment_bucket_map': experimentBucketMap,
  },
};

function App() {
  return (
    <OptimizelyProvider
      optimizely={optimizely}
      user={user}
    >
    {/* … your application components here … */}
    </OptimizelyProvider>
  </App>
}

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

import React from 'react';
import {
  createInstance,
  OptimizelyProvider,
} from '@optimizely/react-sdk'

// This dB is only an example; in a production environment, access a real datastore
class UserProfileDB {
  constructor() {
    /* Example structure
     * {
     *   user1: {
     *     user_id: 'user1',
     *     experiment_bucket_map: {
     *       '12095834311': { // experimentId
     *         variation_id: '12117244349' // variationId
     *       }
     *     }
     *   }
     * }
     */
    this.db = {}
  }

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

  lookup(userId) {
    return new Promise((resolve, reject) => {
      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) {
    // In our case we will not implement this function here. We will look up the attributes for the user below.
  },
  save(userProfileMap) {
    const { user_id, experiment_bucket_map } = userProfileMap
    userDb.save(user_id, experiment_bucket_map)
  }
}

const client = createInstance({
  datafile: optimizelyDatafile, // assuming you have a hardcoded datafile
  userProfileService,
})

// React SDK supports passing a Promise as user, for async user lookups like this:
const user = userDb.lookup(userId).then((experimentBucketMap = {}) => {
  return {
    id: 'user1',
    attributes: {
      $opt_experiment_bucket_map: experimentBucketMap
    },
  }
})

// The provider will use the given user and Optimizely instance.
// The provided experiment bucket map will force any specified variation
// assignments from userDb.
// The provided user profile service will save any new variation assignments to
//userDb.
function App() {
  return (
    <OptimizelyProvider
      optimizely={optimizely}
      user={user}
    >
    {/* … your application components here … */}
    </OptimizelyProvider>
  </App>
}

Did this page help you?