8aac8c52cc
CI / flutter-gates (push) Successful in 2m41s
- home_page Feed segment 改接 CommunityController:骨架屏首载、空态 CTA、 错误横幅重试、下拉刷新(失败保留旧列表 + SnackBar)、触底游标翻页 (失败态只走显式重试,不随滚动风暴自动重打) - PostCard 三形态(单图通栏 4:3 / 多图折叠封面 +N 角标 / 纯文字 6 行)+ PostMediaGrid(列数规则 + ink 80% scrim 精算)+ LikeButton(error 族修订 Colors.red,T3-14 纯展示禁用态,ToggleSync 接线留 T3-15/16)+ FeedSkeleton (呼吸动效尊重减弱动态设置);降级作者统一「宠友」占位 - SignedNetworkImage:预签名 URL 缓存 key 剥离 X-Amz-* 签名参数, RemoteImage 全仓换用(同图不同签名命中同一缓存条目) - feed 域埋点:feed_viewed 聚合曝光(浏览段开/结算于切 Tab、切分段、 退后台、销毁;≥50% 可见 ≥500ms 段内按帖去重,postId 不上报)+ feed_load_failed(refresh/load_more 双路,会话失效不报) - integration_test/feed_live_test.dart:compose 六容器 Linux 桌面真链路 实测入口(PATBOND_FEED_LIVE=1 门控,默认跳过) - 测试 379 → 421(+42):四态/尾部三态/翻页不丢不重/降级作者/曝光结算 时机/缓存 key/事件属性全覆盖;analyze 0,format 无 diff Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
115 lines
3.7 KiB
Dart
115 lines
3.7 KiB
Dart
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,
|
||
);
|
||
}
|
||
}
|