
AB测试可视化看板设计与实现 原创
AB测试可视化看板设计与实现
一、项目概述
基于HarmonyOS 5和AGC(华为应用市场服务)Remote Config构建的AB测试可视化看板系统,用于实时展示不同实验组的转化率对比数据。该系统借鉴《鸿蒙跨端U同步》中游戏场景的多设备数据同步机制,实现实验配置的实时更新和用户行为数据的跨设备采集,为产品决策提供数据支持。
二、核心架构设计
±--------------------+
AGC Remote Config
(实验配置中心)
±---------±---------+
±---------v----------+ ±--------------------+
数据采集SDK <—> 用户行为分析服务
(Data Collector) (Analytics)
±---------±---------+ ±--------------------+
±---------v----------+
可视化看板
(Dashboard)
±--------------------+
三、AGC Remote Config集成
// Remote Config管理类
public class RemoteConfigManager {
private static final String TAG = “RemoteConfig”;
private AGConnectRemoteConfig config;
public RemoteConfigManager(Context context) {
AGConnectInstance.initialize(context);
this.config = AGConnectRemoteConfig.getInstance();
// 设置默认参数
Map<String, Object> defaults = new HashMap<>();
defaults.put("ab_test_group", "control");
defaults.put("new_feature_enabled", false);
config.applyDefault(defaults);
// 获取用户分组
public String getABTestGroup() {
return config.getValueAsString("ab_test_group");
// 检查新功能是否启用
public boolean isNewFeatureEnabled() {
return config.getValueAsBoolean("new_feature_enabled");
// 获取配置参数
public String getConfigValue(String key) {
return config.getValueAsString(key);
// 刷新远程配置
public void fetchConfig(ConfigCallback callback) {
config.fetch().addOnSuccessListener(configValues -> {
config.apply(configValues);
callback.onSuccess();
}).addOnFailureListener(e -> {
callback.onError(e);
});
public interface ConfigCallback {
void onSuccess();
void onError(Exception e);
}
四、数据采集实现
// 用户行为数据采集器
public class AnalyticsCollector {
private static final String TAG = “Analytics”;
private HiAnalyticsInstance analytics;
private RemoteConfigManager configManager;
public AnalyticsCollector(Context context) {
this.analytics = HiAnalytics.getInstance(context);
this.configManager = new RemoteConfigManager(context);
// 记录用户事件
public void logEvent(String eventName, Bundle params) {
// 添加AB测试分组信息
params.putString("ab_test_group", configManager.getABTestGroup());
analytics.onEvent(eventName, params);
// 记录转化事件
public void logConversion(String conversionPoint) {
Bundle params = new Bundle();
params.putString("conversion_point", conversionPoint);
params.putString("ab_test_group", configManager.getABTestGroup());
analytics.onEvent("conversion", params);
// 记录页面访问
public void logPageView(String pageName) {
Bundle params = new Bundle();
params.putString("page_name", pageName);
params.putString("ab_test_group", configManager.getABTestGroup());
analytics.onEvent("page_view", params);
}
五、可视化看板实现
数据获取服务
// AB测试数据服务
public class ABTestDataService {
private static final String TAG = “ABTestData”;
private AGConnectCloudDB cloudDB;
public ABTestDataService(Context context) {
CloudDBZoneConfig config = new CloudDBZoneConfig("abtest_data",
CloudDBZoneConfig.CloudDBZoneSyncProperty.CLOUDDBZONE_CLOUD_CACHE,
CloudDBZoneConfig.CloudDBZoneAccessProperty.CLOUDDBZONE_PUBLIC);
AGConnectCloudDB.initialize(context);
cloudDB = AGConnectCloudDB.getInstance();
cloudDB.createCloudDBZone(config);
// 获取实验组转化数据
public void getConversionData(String experimentId, DataCallback callback) {
CloudDBZoneQuery<ABTestResult> query = CloudDBZoneQuery.where(ABTestResult.class)
.equalTo("experimentId", experimentId);
cloudDB.executeQuery(query, QueryPolicy.POLICY_QUERY_FROM_CLOUD_ONLY)
.addOnSuccessListener(snapshot -> {
List<ABTestResult> results = new ArrayList<>();
while (snapshot.hasNext()) {
results.add(snapshot.next());
callback.onDataLoaded(results);
})
.addOnFailureListener(e -> {
callback.onError(e);
});
// 获取多设备实验数据
public void getMultiDeviceData(String experimentId, MultiDeviceCallback callback) {
CloudDBZoneQuery<DeviceExperimentData> query = CloudDBZoneQuery.where(DeviceExperimentData.class)
.equalTo("experimentId", experimentId);
cloudDB.executeQuery(query, QueryPolicy.POLICY_QUERY_FROM_CLOUD_ONLY)
.addOnSuccessListener(snapshot -> {
Map<String, List<DeviceExperimentData>> deviceDataMap = new HashMap<>();
while (snapshot.hasNext()) {
DeviceExperimentData data = snapshot.next();
if (!deviceDataMap.containsKey(data.getDeviceType())) {
deviceDataMap.put(data.getDeviceType(), new ArrayList<>());
deviceDataMap.get(data.getDeviceType()).add(data);
callback.onDataLoaded(deviceDataMap);
})
.addOnFailureListener(e -> {
callback.onError(e);
});
public interface DataCallback {
void onDataLoaded(List<ABTestResult> results);
void onError(Exception e);
public interface MultiDeviceCallback {
void onDataLoaded(Map<String, List<DeviceExperimentData>> deviceData);
void onError(Exception e);
}
数据可视化组件
// 转化率对比图表
public class ConversionChart extends Component {
private Paint groupAPaint;
private Paint groupBPaint;
private Paint axisPaint;
private Paint textPaint;
private List<ABTestResult> results;
private float maxConversionRate = 0;
public ConversionChart(Context context) {
super(context);
groupAPaint = new Paint();
groupAPaint.setColor(0xFF4285F4); // Google Blue
groupAPaint.setStyle(Paint.Style.FILL);
groupBPaint = new Paint();
groupBPaint.setColor(0xFFEA4335); // Google Red
groupBPaint.setStyle(Paint.Style.FILL);
axisPaint = new Paint();
axisPaint.setColor(Color.BLACK);
axisPaint.setStrokeWidth(2);
textPaint = new Paint();
textPaint.setColor(Color.BLACK);
textPaint.setTextSize(24);
public void setData(List<ABTestResult> results) {
this.results = results;
// 计算最大转化率用于缩放
for (ABTestResult result : results) {
if (result.getConversionRate() > maxConversionRate) {
maxConversionRate = result.getConversionRate();
}
invalidate();
@Override
public void onDraw(Component component, Canvas canvas) {
super.onDraw(component, canvas);
if (results == null || results.isEmpty()) {
return;
int width = getWidth();
int height = getHeight();
int padding = 40;
// 绘制坐标轴
canvas.drawLine(padding, height - padding, width - padding, height - padding, axisPaint); // X轴
canvas.drawLine(padding, padding, padding, height - padding, axisPaint); // Y轴
// 绘制刻度
for (int i = 0; i <= 10; i++) {
float y = height - padding - (height - 2 padding) i / 10;
canvas.drawLine(padding - 5, y, padding, y, axisPaint);
canvas.drawText(String.format("%d%%", i * 10), 10, y + 10, textPaint);
// 计算柱状图参数
int barCount = results.size();
float barWidth = (width - 2 padding) / (barCount 3); // 每组两个柱状图加间距
float startX = padding + barWidth;
// 绘制柱状图
for (int i = 0; i < barCount; i++) {
ABTestResult result = results.get(i);
// 实验组A
float barHeightA = (height - 2 padding) result.getGroupAConversion() / maxConversionRate;
canvas.drawRect(
startX,
height - padding - barHeightA,
startX + barWidth,
height - padding,
groupAPaint
);
// 实验组B
float barHeightB = (height - 2 padding) result.getGroupBConversion() / maxConversionRate;
canvas.drawRect(
startX + barWidth * 1.5f,
height - padding - barHeightB,
startX + barWidth * 2.5f,
height - padding,
groupBPaint
);
// 绘制标签
canvas.drawText(
result.getDate().substring(5), // 显示月日
startX + barWidth * 1.25f,
height - padding / 2,
textPaint
);
startX += barWidth * 3;
// 绘制图例
canvas.drawText("对照组", width - 150, 50, textPaint);
canvas.drawRect(width - 180, 35, width - 160, 55, groupAPaint);
canvas.drawText("实验组", width - 150, 80, textPaint);
canvas.drawRect(width - 180, 65, width - 160, 85, groupBPaint);
}
多设备数据展示
// 多设备数据对比组件
public class DeviceComparisonChart extends Component {
private Paint[] devicePaints;
private Paint axisPaint;
private Paint textPaint;
private Map<String, List<DeviceExperimentData>> deviceData;
public DeviceComparisonChart(Context context) {
super(context);
// 初始化不同设备的颜色
devicePaints = new Paint[5];
int[] colors = {0xFF4285F4, 0xFFEA4335, 0xFFFBBC05, 0xFF34A853, 0xFF673AB7};
for (int i = 0; i < devicePaints.length; i++) {
devicePaints[i] = new Paint();
devicePaints[i].setColor(colors[i]);
devicePaints[i].setStyle(Paint.Style.FILL);
axisPaint = new Paint();
axisPaint.setColor(Color.BLACK);
axisPaint.setStrokeWidth(2);
textPaint = new Paint();
textPaint.setColor(Color.BLACK);
textPaint.setTextSize(24);
public void setDeviceData(Map<String, List<DeviceExperimentData>> deviceData) {
this.deviceData = deviceData;
invalidate();
@Override
public void onDraw(Component component, Canvas canvas) {
super.onDraw(component, canvas);
if (deviceData == null || deviceData.isEmpty()) {
return;
int width = getWidth();
int height = getHeight();
int padding = 40;
// 绘制坐标轴
canvas.drawLine(padding, height - padding, width - padding, height - padding, axisPaint); // X轴
canvas.drawLine(padding, padding, padding, height - padding, axisPaint); // Y轴
// 绘制刻度
for (int i = 0; i <= 10; i++) {
float y = height - padding - (height - 2 padding) i / 10;
canvas.drawLine(padding - 5, y, padding, y, axisPaint);
canvas.drawText(String.format("%d%%", i * 10), 10, y + 10, textPaint);
// 准备设备数据
List<String> deviceTypes = new ArrayList<>(deviceData.keySet());
int deviceCount = Math.min(deviceTypes.size(), devicePaints.length);
// 计算柱状图参数
float barWidth = (width - 2 padding) / (deviceCount 2);
float startX = padding + barWidth;
// 绘制每个设备的转化率
for (int i = 0; i < deviceCount; i++) {
String deviceType = deviceTypes.get(i);
List<DeviceExperimentData> dataList = deviceData.get(deviceType);
// 计算该设备类型的平均转化率
float totalConversion = 0;
for (DeviceExperimentData data : dataList) {
totalConversion += data.getConversionRate();
float avgConversion = totalConversion / dataList.size();
// 绘制柱状图
float barHeight = (height - 2 padding) avgConversion / 100;
canvas.drawRect(
startX,
height - padding - barHeight,
startX + barWidth,
height - padding,
devicePaints[i]
);
// 绘制设备标签
String shortName = deviceType.length() > 8 ?
deviceType.substring(0, 5) + "..." : deviceType;
canvas.drawText(
shortName,
startX,
height - padding / 2,
textPaint
);
startX += barWidth * 1.5f;
// 绘制图例
int legendY = 50;
for (int i = 0; i < deviceCount; i++) {
canvas.drawRect(width - 180, legendY - 15, width - 160, legendY + 5, devicePaints[i]);
canvas.drawText(deviceTypes.get(i), width - 150, legendY, textPaint);
legendY += 30;
}
六、完整看板实现
// AB测试看板Ability
public class ABTestDashboardAbilitySlice extends AbilitySlice {
private ConversionChart conversionChart;
private DeviceComparisonChart deviceChart;
private Text statsTextView;
private ABTestDataService dataService;
private RemoteConfigManager configManager;
@Override
public void onStart(Intent intent) {
super.onStart(intent);
setUIContent(ResourceTable.Layout_abtest_dashboard);
// 初始化UI组件
conversionChart = (ConversionChart) findComponentById(ResourceTable.Id_conversion_chart);
deviceChart = (DeviceComparisonChart) findComponentById(ResourceTable.Id_device_chart);
statsTextView = (Text) findComponentById(ResourceTable.Id_stats_text);
// 初始化服务
dataService = new ABTestDataService(this);
configManager = new RemoteConfigManager(this);
// 加载当前实验配置
loadCurrentExperiment();
private void loadCurrentExperiment() {
String currentExperiment = configManager.getConfigValue("current_experiment");
if (currentExperiment != null && !currentExperiment.isEmpty()) {
refreshData(currentExperiment);
}
private void refreshData(String experimentId) {
// 加载转化率数据
dataService.getConversionData(experimentId, new ABTestDataService.DataCallback() {
@Override
public void onDataLoaded(List<ABTestResult> results) {
getUITaskDispatcher().asyncDispatch(() -> {
conversionChart.setData(results);
updateStatsText(results);
});
@Override
public void onError(Exception e) {
showError("加载转化数据失败: " + e.getMessage());
});
// 加载多设备数据
dataService.getMultiDeviceData(experimentId, new ABTestDataService.MultiDeviceCallback() {
@Override
public void onDataLoaded(Map<String, List<DeviceExperimentData>> deviceData) {
getUITaskDispatcher().asyncDispatch(() -> {
deviceChart.setDeviceData(deviceData);
});
@Override
public void onError(Exception e) {
showError("加载设备数据失败: " + e.getMessage());
});
private void updateStatsText(List<ABTestResult> results) {
if (results == null || results.isEmpty()) {
return;
ABTestResult latest = results.get(results.size() - 1);
double lift = (latest.getGroupBConversion() - latest.getGroupAConversion()) /
latest.getGroupAConversion() * 100;
String statsText = String.format(Locale.getDefault(),
"最新数据:\n" +
"对照组转化率: %.2f%%\n" +
"实验组转化率: %.2f%%\n" +
"提升效果: %.1f%%\n" +
"统计显著性: %s",
latest.getGroupAConversion(),
latest.getGroupBConversion(),
lift,
latest.isSignificant() ? "显著" : "不显著"
);
statsTextView.setText(statsText);
private void showError(String message) {
getUITaskDispatcher().asyncDispatch(() -> {
new ToastDialog(this)
.setText(message)
.show();
});
}
七、技术创新点
实时数据同步:基于AGC Remote Config实现实验配置实时更新
多维度分析:支持时间趋势和多设备类型对比
可视化丰富:提供直观的图表展示转化率差异
跨设备支持:借鉴鸿蒙跨端同步机制实现多设备数据采集
决策支持:自动计算提升效果和统计显著性
八、总结
本AB测试可视化看板系统基于HarmonyOS 5和AGC服务,实现了以下核心价值:
实验监控:实时跟踪不同实验组的表现差异
数据驱动:为产品决策提供可靠的量化依据
效率提升:快速验证新功能或改版效果
跨端一致:确保多设备用户的实验体验一致
系统借鉴了《鸿蒙跨端U同步》中的数据同步和展示机制,将游戏场景的实时同步需求应用于产品实验分析领域。未来可结合预测模型预估实验效果,并与功能发布系统集成,实现从实验到发布的自动化流程。
