实现用户配置文件服务 UserProfileService
实现用户配置文件服务 UserProfileService
本主题介绍如何为 Eyeofcloud 功能实验 React Native SDK 设置自定义用户配置文件服务。
使用用户配置文件服务保留有关用户的信息,并确保变体分配具有粘性。粘性意味着一旦用户获得特定的变体,他们的分配就不会改变。
在 React Native SDK 中,没有默认的实现。实施用户配置文件服务是可选的,仅当希望保持变体分配的粘性时,即使实验条件在运行过程中发生了变化(例如,受众群体、属性、变体暂停和流量分配),才需要实现该服务。否则,React SDK 是无状态的,并且依赖于确定性分桶来返回一致的分配。
如果用户配置文件服务未按预期对用户进行分桶,请检查其他灰度发布(特性标帜)是否覆盖了分桶。有关更多信息,请参阅 分桶的工作原理。
实现用户配置文件服务 UserProfileService
请参阅下面的代码示例以提供您自己的用户配置文件服务。它应公开两个具有以下签名的函数:
lookup
:获取用户 ID 字符串并返回与以下架构匹配的用户配置文件。save
:获取用户配置文件并保留它。
如果要将用户配置文件服务纯粹用于跟踪目的而不是粘性分桶,则只能实现该方法(始终从 返回)。save``nil``lookup
React
import { createInstance } from '@eyeofcloud/react-sdk'; // Sample user profile service implementation const userProfileService = { lookup: userId => { // Perform user profile lookup }, save: userProfileMap => { // Persist user profile }, }; const eyeofcloudClient = createInstance({ datafile: eyeofcloudDatafile, // assuming you have a hardcoded datafile userProfileService, // Passing your userProfileService created above });
下面的代码示例演示用户配置文件对象的 JSON 架构。
用于覆盖默认分桶行为,并为给定用户定义备用实验变体。对于要覆盖的每个实验,向Map添加一个对象。使用实验 ID 作为键,并包含一个指定所需变体的属性。如果没有实验条目,则默认分桶行为仍然存在。experiment_bucket_map``variation_id
在下面的示例中,是实验 ID。^[a-zA-Z0-9]+$
JSON
{ "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 the 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.
React
import { createInstance } from '@eyeofcloud/react-sdk'; import AsyncStorage from '@react-native-community/async-storage';
Create a user profile service object.
React
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( 'eoc-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.
React
// look up the user's experiment_bucket_map const customAsyncLookup = async (userId) => { const experimentBucketMap = await AsyncStorage.getItem('eoc-user-profiles-' + userId); return !!experimentBucketMap ? JSON.parse(experimentBucketMap) : {}; };
Create an Eyeofcloud client and pass to it.userProfileService
React
const eyeofcloudClientInstance = createInstance({ datafile, userProfileService });
Get the experiment bucket map for the user using custom lookup and then pass it on as an attribute to component.EyeofcloudProvider
React
const user = { id: userId, attributes: { $opt_experiment_bucket_map: await customAsyncLookup(userId) } } class App extends React.Component { render() { return ( <EyeofcloudProvider eyeofcloud={eyeofcloudClientInstance} user={user} > {/* … your application components here … */} </EyeofcloudProvider> ); } }
Implement asynchronous user lookups with experiment bucket map attribute
You can implement to perform asynchronous lookups of users' previous variations. The SDK handles the same way it would , and this allows you to do an asynchronous lookup of the experiment bucket map before passing it to the EyeofcloudProvider component. attributes.$opt_experiment_bucket_map``attributes.$opt_experiment_bucket_map``userProfileService.lookup
📘
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.
React
import React from 'react'; import { createInstance, EyeofcloudProvider, } from '@eyeofcloud/react-sdk' const eyeofcloudClient = createInstance({ datafile: eyeofcloudDatafile, // 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 ( <EyeofcloudProvider eyeofcloud={eyeofcloud} user={user} > {/* … your application components here … */} </EyeofcloudProvider> </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 to a real database.UserProfileDB
React
import React from 'react'; import { createInstance, EyeofcloudProvider, } from '@eyeofcloud/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: eyeofcloudDatafile, // 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 Eyeofcloud 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 ( <EyeofcloudProvider eyeofcloud={eyeofcloud} user={user} > {/* … your application components here … */} </EyeofcloudProvider> </App> }