新增:pet 域埋点强类型封装(06 号规划字典 v2 三事件)
- pet_create_started / succeeded / failed 三事件(13 号规范 §3.1 强类型惯例:枚举编译期锁死,业务代码禁止手拼事件名与属性) - entryPoint 三枚举(post_register_guide 预留);failureReason 四枚举 (pet_limit_reached 因产品未设上限未纳入;5xx 与断网/超时客户端 不可区分,并入 network_error) - errorCode 携带业务码并推导 httpStatus(code ~/ 100);attemptSeq 记录表单会话内提交序号 - 单元测试 4 个(属性齐备、可空属性缺席语义) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,83 @@
|
||||
import 'package:patbond_flutter/analytics/page_view_tracker.dart';
|
||||
import 'package:patbond_flutter/features/pets/pet_models.dart';
|
||||
|
||||
/// pet 域埋点强类型封装(06 号规划 §1.4/§4 字典 v2 三事件;沿用
|
||||
/// 13 号规范 §3.1 惯例:枚举编译期锁死,业务代码禁止手拼事件名与属性)。
|
||||
///
|
||||
/// 三事件只覆盖「建宠」漏斗:`page_viewed(pet_form)` 承接到达段(路由
|
||||
/// 埋点自动采集),started 在表单**首次输入**触发、每次进入记一次;
|
||||
/// 编辑不设事件(06 §1.4 自觉取舍:短表单漏斗价值低于事件成本)。
|
||||
|
||||
/// 建宠表单入口(06 §4:pet_create_started.entryPoint 枚举)。
|
||||
/// post_register_guide 预留:注册后引导流尚未落地,落地时启用。
|
||||
enum PetCreateEntryPoint {
|
||||
profileEmptyState('profile_empty_state'),
|
||||
petList('pet_list'),
|
||||
postRegisterGuide('post_register_guide');
|
||||
|
||||
const PetCreateEntryPoint(this.value);
|
||||
|
||||
final String value;
|
||||
}
|
||||
|
||||
/// 建宠失败原因(06 §4 枚举;pet_limit_reached 因产品未设上限未纳入,
|
||||
/// 若后续拍板设上限随字典增补)。客户端网络层不区分 5xx 与断网/超时
|
||||
/// (均为 ApiNetworkException),两者并入 network_error,server_error
|
||||
/// 保留给无法归类的兜底。
|
||||
enum PetCreateFailureReason {
|
||||
validationError('validation_error'),
|
||||
rateLimited('rate_limited'),
|
||||
networkError('network_error'),
|
||||
serverError('server_error');
|
||||
|
||||
const PetCreateFailureReason(this.value);
|
||||
|
||||
final String value;
|
||||
}
|
||||
|
||||
class PetAnalytics {
|
||||
PetAnalytics(this._track);
|
||||
|
||||
/// 生产传 `AnalyticsService.trackEvent`,测试传录制桩。
|
||||
final TrackEventFn _track;
|
||||
|
||||
/// 进入建宠表单并产生首次输入(每次进入记一次,表单层负责去重)。
|
||||
void createStarted({required PetCreateEntryPoint entryPoint}) {
|
||||
_track('pet_create_started', {'entryPoint': entryPoint.value});
|
||||
}
|
||||
|
||||
/// 建宠接口成功响应(漏斗事件)。
|
||||
///
|
||||
/// [durationMs] 为表单打开到成功的耗时;[petIndex] 为该用户第几只宠物
|
||||
/// (H2 假设的直接数据源,与 pet.pets 事实表交叉验证)。
|
||||
void createSucceeded({
|
||||
required int durationMs,
|
||||
required PetSpecies species,
|
||||
required int petIndex,
|
||||
}) {
|
||||
_track('pet_create_succeeded', {
|
||||
'durationMs': durationMs,
|
||||
'species': species.name,
|
||||
'petIndex': petIndex,
|
||||
});
|
||||
}
|
||||
|
||||
/// 建宠失败:失败响应 / 超时 / 本地校验拦截。
|
||||
///
|
||||
/// [errorCode] 为业务错误码(本地校验/网络错误时缺席);
|
||||
/// [httpStatus] 由五位业务码推导(`code ~/ 100`);
|
||||
/// [attemptSeq] 为本次表单会话内第几次提交尝试(从 1 起)。
|
||||
void createFailed({
|
||||
required PetCreateFailureReason reason,
|
||||
required int attemptSeq,
|
||||
int? errorCode,
|
||||
}) {
|
||||
_track('pet_create_failed', {
|
||||
'failureReason': reason.value,
|
||||
'attemptSeq': attemptSeq,
|
||||
'errorCode': ?errorCode,
|
||||
if (errorCode != null && errorCode >= 10000)
|
||||
'httpStatus': errorCode ~/ 100,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:patbond_flutter/features/pets/pet_analytics.dart';
|
||||
import 'package:patbond_flutter/features/pets/pet_models.dart';
|
||||
|
||||
void main() {
|
||||
late List<(String, Map<String, dynamic>?)> events;
|
||||
late PetAnalytics analytics;
|
||||
|
||||
setUp(() {
|
||||
events = [];
|
||||
analytics = PetAnalytics(
|
||||
(name, [props]) async => events.add((name, props)),
|
||||
);
|
||||
});
|
||||
|
||||
test('pet_create_started:entryPoint 枚举值上报', () {
|
||||
analytics.createStarted(entryPoint: PetCreateEntryPoint.profileEmptyState);
|
||||
|
||||
expect(events.single.$1, 'pet_create_started');
|
||||
expect(events.single.$2, {'entryPoint': 'profile_empty_state'});
|
||||
});
|
||||
|
||||
test('pet_create_succeeded:durationMs/species/petIndex 三属性齐备', () {
|
||||
analytics.createSucceeded(
|
||||
durationMs: 1234,
|
||||
species: PetSpecies.cat,
|
||||
petIndex: 2,
|
||||
);
|
||||
|
||||
expect(events.single.$1, 'pet_create_succeeded');
|
||||
expect(events.single.$2, {
|
||||
'durationMs': 1234,
|
||||
'species': 'cat',
|
||||
'petIndex': 2,
|
||||
});
|
||||
});
|
||||
|
||||
test('pet_create_failed:业务码携带 errorCode 并推导 httpStatus', () {
|
||||
analytics.createFailed(
|
||||
reason: PetCreateFailureReason.validationError,
|
||||
attemptSeq: 2,
|
||||
errorCode: 40903,
|
||||
);
|
||||
|
||||
expect(events.single.$1, 'pet_create_failed');
|
||||
expect(events.single.$2, {
|
||||
'failureReason': 'validation_error',
|
||||
'attemptSeq': 2,
|
||||
'errorCode': 40903,
|
||||
'httpStatus': 409,
|
||||
});
|
||||
});
|
||||
|
||||
test('pet_create_failed:本地校验/网络失败无 errorCode 时两可空属性缺席', () {
|
||||
analytics.createFailed(
|
||||
reason: PetCreateFailureReason.networkError,
|
||||
attemptSeq: 1,
|
||||
);
|
||||
|
||||
expect(events.single.$2, {
|
||||
'failureReason': 'network_error',
|
||||
'attemptSeq': 1,
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user