import 'package:patbond_flutter/analytics/page_view_tracker.dart'; import 'package:patbond_flutter/core/network/api_exception.dart'; /// 互动域埋点强类型封装(06 号规划 §1.4 字典 v3 互动八事件;后端白名单 /// 已随 api dev@8089c06 就绪,22 号报告 §1)。沿 pet/feed 域惯例:枚举 /// 编译期锁死,业务代码禁止手拼事件名与属性;只记行为不记内容 /// (隐私红线:postId/commentId 等内容 ID 一律不进 props)。 /// /// 点赞/收藏/关注**不埋失败**(06 §1.4 取舍:幂等写入单点交互,失败率 /// 靠服务端错误率观测);`comment_create_started` 被字典锁死 unknown, /// 不得上报(22 号 §1 末段)。 /// 互动触点来源(06 §1.4 source 枚举)。M3 接 feed / post_detail; /// user_profile / follow_list 随后续页面启用。 enum InteractionSource { feed('feed'), postDetail('post_detail'), userProfile('user_profile'), followList('follow_list'); const InteractionSource(this.value); final String value; } /// 评论创建失败原因(06 §1.4 失败枚举基底)。网络归并口径同 pet/feed 域: /// 断网/超时/5xx 均并入 network_error,server_error 保留兜底。 enum CommentCreateFailureReason { validationError('validation_error'), notFound('not_found'), rateLimited('rate_limited'), networkError('network_error'), serverError('server_error'); const CommentCreateFailureReason(this.value); final String value; } /// 类型化异常 → 评论失败原因;会话失效返回 null(应用即将回登录页, /// 不作为评论失败上报,feed 域同款口径)。 CommentCreateFailureReason? commentCreateFailureReasonOf(ApiException error) => switch (error) { ApiNetworkException _ => CommentCreateFailureReason.networkError, ApiRateLimitException _ => CommentCreateFailureReason.rateLimited, SessionExpiredException _ => null, ApiBusinessException(:final code) => switch (code) { ApiCodes.paramError => CommentCreateFailureReason.validationError, ApiCodes.postNotFound || ApiCodes.commentNotFound || ApiCodes.communityUserNotFound => CommentCreateFailureReason.notFound, _ => CommentCreateFailureReason.serverError, }, }; /// 正文规模分桶(06 §1.3 隐私红线 1:不报精确字数)。 /// empty / short(≤50) / medium(51–500) / long(>500)。 String textLengthBucketOf(int length) { if (length <= 0) return 'empty'; if (length <= 50) return 'short'; if (length <= 500) return 'medium'; return 'long'; } class CommunityInteractionAnalytics { CommunityInteractionAnalytics(this._track); /// 生产传 `AnalyticsService.trackEvent`,测试传录制桩。 final TrackEventFn _track; /// 点赞成功响应后(乐观翻转本身不报;单飞合并链每个实际抵达服务端 /// 并成功的状态变更各报一条,与「成功响应后」的字典口径一致)。 void postLiked({required InteractionSource source}) { _track('post_liked', {'source': source.value}); } /// 取消点赞成功响应后。 void postUnliked({required InteractionSource source}) { _track('post_unliked', {'source': source.value}); } /// 收藏成功响应后。 void postFavorited({required InteractionSource source}) { _track('post_favorited', {'source': source.value}); } /// 取消收藏成功响应后。 void postUnfavorited({required InteractionSource source}) { _track('post_unfavorited', {'source': source.value}); } /// 评论提交成功响应后。 /// /// [durationMs]:本次输入会话(首个字符输入)→ 成功响应的耗时 /// (评论不设 started 事件,时长随成功事件带出); /// [textLength] 经 [textLengthBucketOf] 分桶后上报,精确字数不出端。 void commentCreateSucceeded({ required int durationMs, required bool isReply, required int textLength, }) { _track('comment_create_succeeded', { 'durationMs': durationMs, 'isReply': isReply, 'textLengthBucket': textLengthBucketOf(textLength), }); } /// 评论提交失败。 /// /// [errorCode] 为业务错误码(网络错误时缺席);[httpStatus] 由五位 /// 业务码推导(`code ~/ 100`,pet 域同款口径);[attemptSeq] 为本次 /// 输入会话内第几次提交尝试(从 1 起,成功或清空输入后重置)。 void commentCreateFailed({ required CommentCreateFailureReason reason, required int attemptSeq, int? errorCode, }) { _track('comment_create_failed', { 'failureReason': reason.value, 'attemptSeq': attemptSeq, 'errorCode': ?errorCode, if (errorCode != null && errorCode >= 10000) 'httpStatus': errorCode ~/ 100, }); } /// 关注成功响应后。 void userFollowed({required InteractionSource source}) { _track('user_followed', {'source': source.value}); } /// 取关成功响应后。 void userUnfollowed({required InteractionSource source}) { _track('user_unfollowed', {'source': source.value}); } }