Files
lixi 8aac8c52cc
CI / flutter-gates (push) Successful in 2m41s
新增:首页 Feed 接入真实数据——四态/尾部三态/聚合曝光埋点,社区 demo 消亡第一页(T3-14)
- 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>
2026-09-09 13:01:43 +08:00

115 lines
3.7 KiB
Dart
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 仅接 hometopic / 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_errorserver_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,
);
}
}