import 'package:patbond_flutter/analytics/page_view_tracker.dart'; import 'package:patbond_flutter/core/network/api_exception.dart'; /// feed 域埋点强类型封装(06 号规划 §1.4 字典 v3;后端白名单已随 /// api dev@8089c06 就绪)。沿 pet_analytics 惯例:枚举编译期锁死, /// 业务代码禁止手拼事件名与属性;只记行为不记内容(隐私红线:postId /// 等内容 ID 一律不进 props,曝光去重键只存活于客户端内存)。 /// feed_viewed / feed_load_failed 的 feedTab 枚举(06 §1.4)。 /// M3 T3-14 仅接 home;topic / user_posts / favorites 随后续页面启用。 enum FeedTab { home('home'), topic('topic'), userPosts('user_posts'), favorites('favorites'); const FeedTab(this.value); final String value; } /// feed_load_failed 的 loadType 枚举。 enum FeedLoadType { refresh('refresh'), loadMore('load_more'); const FeedLoadType(this.value); final String value; } /// feed 加载失败原因(06 v3 失败枚举基底)。网络归并口径同 pet 域: /// 断网/超时/5xx 均并入 network_error,server_error 保留兜底。 enum FeedLoadFailureReason { rateLimited('rate_limited'), networkError('network_error'), serverError('server_error'); const FeedLoadFailureReason(this.value); final String value; } /// 类型化异常 → 失败原因;会话失效返回 null(应用即将回登录页, /// 不作为 Feed 加载失败上报)。 FeedLoadFailureReason? feedLoadFailureReasonOf(ApiException error) => switch (error) { ApiNetworkException _ => FeedLoadFailureReason.networkError, ApiRateLimitException _ => FeedLoadFailureReason.rateLimited, SessionExpiredException _ => null, _ => FeedLoadFailureReason.serverError, }; class FeedAnalytics { FeedAnalytics(this._track); /// 生产传 `AnalyticsService.trackEvent`,测试传录制桩。 final TrackEventFn _track; /// 一个 Feed 浏览段的聚合曝光(06 §1.2 裁定):离开 Feed(路由跳走 / /// 退后台)时发一条,携带段内曝光卡片数(≥50% 可见 ≥500ms、按帖去重 /// 后的**计数**)、翻页数、刷新数与前台停留时长。 void feedViewed({ required FeedTab feedTab, required int durationMs, required int impressionCount, required int loadMoreCount, required int refreshCount, }) { _track('feed_viewed', { 'feedTab': feedTab.value, 'durationMs': durationMs, 'impressionCount': impressionCount, 'loadMoreCount': loadMoreCount, 'refreshCount': refreshCount, }); } /// 刷新或翻页请求失败(M3 验收「分页不丢失不重复」的客户端观测点)。 /// /// [errorCode] 为业务错误码(网络错误时缺席);[httpStatus] 由五位 /// 业务码推导(`code ~/ 100`,pet 域同款口径)。 void feedLoadFailed({ required FeedTab feedTab, required FeedLoadType loadType, required FeedLoadFailureReason reason, int? errorCode, }) { _track('feed_load_failed', { 'feedTab': feedTab.value, 'loadType': loadType.value, 'failureReason': reason.value, 'errorCode': ?errorCode, if (errorCode != null && errorCode >= 10000) 'httpStatus': errorCode ~/ 100, }); } /// [feedLoadFailed] 的异常直通口:会话失效不上报。 void feedLoadFailedFrom( ApiException error, { required FeedTab feedTab, required FeedLoadType loadType, }) { final reason = feedLoadFailureReasonOf(error); if (reason == null) return; feedLoadFailed( feedTab: feedTab, loadType: loadType, reason: reason, errorCode: error is ApiBusinessException ? error.code : null, ); } }