新增:首页 Feed 接入真实数据——四态/尾部三态/聚合曝光埋点,社区 demo 消亡第一页(T3-14)
CI / flutter-gates (push) Successful in 2m41s
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>
This commit is contained in:
@@ -0,0 +1,110 @@
|
||||
import 'dart:async';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:patbond_flutter/features/community/feed_analytics.dart';
|
||||
|
||||
/// 一个「Feed 浏览段」的聚合器(06 号规划 §1.2 裁定的客户端实现)。
|
||||
///
|
||||
/// 段生命周期由页面驱动:进入 Feed(Tab 激活且分段在 Feed)开段;
|
||||
/// 离开(切 Tab / 切分段 / 退后台 / 页面销毁)时 [settle] 结算并经
|
||||
/// [FeedAnalytics.feedViewed] 发**一条**聚合事件。
|
||||
///
|
||||
/// - 曝光判定:卡片可见面积 ≥ [visibleThreshold] 且持续 ≥ [dwell],
|
||||
/// 段内按 postId 去重;postId 只作内存去重键、绝不上报(隐私红线 2),
|
||||
/// 段结束即弃。
|
||||
/// - durationMs 为前台时长(退后台即结算,段天然前台连续),上限
|
||||
/// 截断 30 分钟防挂机污染(06 §1.4 实现注意)。
|
||||
class FeedViewSegment {
|
||||
FeedViewSegment({DateTime Function()? now})
|
||||
: _now = now ?? DateTime.now,
|
||||
_settled = false {
|
||||
_startedAt = _now();
|
||||
}
|
||||
|
||||
static const visibleThreshold = 0.5;
|
||||
static const dwell = Duration(milliseconds: 500);
|
||||
static const maxDuration = Duration(minutes: 30);
|
||||
|
||||
final DateTime Function() _now;
|
||||
late final DateTime _startedAt;
|
||||
|
||||
final Set<String> _impressed = <String>{};
|
||||
final Map<String, Timer> _dwellTimers = <String, Timer>{};
|
||||
int _loadMoreCount = 0;
|
||||
int _refreshCount = 0;
|
||||
bool _settled;
|
||||
|
||||
/// 段内曝光卡片数(已去重;仅测试与结算读取)。
|
||||
int get impressionCount => _impressed.length;
|
||||
|
||||
/// 可见性回报:≥50% 起 500ms 驻留计时,跌破或滚出视口即取消;
|
||||
/// 驻留满即记曝光(去重后不再计时)。
|
||||
void updateVisibility(String postId, double visibleFraction) {
|
||||
if (_settled) return;
|
||||
if (visibleFraction >= visibleThreshold) {
|
||||
if (_impressed.contains(postId) || _dwellTimers.containsKey(postId)) {
|
||||
return;
|
||||
}
|
||||
_dwellTimers[postId] = Timer(dwell, () {
|
||||
_dwellTimers.remove(postId);
|
||||
_impressed.add(postId);
|
||||
});
|
||||
} else {
|
||||
_dwellTimers.remove(postId)?.cancel();
|
||||
}
|
||||
}
|
||||
|
||||
/// 用户触发的下拉刷新 / 失败重试(首屏自动预取不计)。
|
||||
void recordRefresh() {
|
||||
if (!_settled) _refreshCount += 1;
|
||||
}
|
||||
|
||||
/// 触底翻页请求(含尾部失败重试)。
|
||||
void recordLoadMore() {
|
||||
if (!_settled) _loadMoreCount += 1;
|
||||
}
|
||||
|
||||
/// 结算:取消在途驻留计时并冻结计数;幂等(重复调用返回 null,
|
||||
/// 保证一段恰好一条 feed_viewed)。
|
||||
FeedViewSummary? settle() {
|
||||
if (_settled) return null;
|
||||
_settled = true;
|
||||
for (final timer in _dwellTimers.values) {
|
||||
timer.cancel();
|
||||
}
|
||||
_dwellTimers.clear();
|
||||
final elapsed = _now().difference(_startedAt);
|
||||
return FeedViewSummary(
|
||||
durationMs: min(elapsed.inMilliseconds, maxDuration.inMilliseconds),
|
||||
impressionCount: _impressed.length,
|
||||
loadMoreCount: _loadMoreCount,
|
||||
refreshCount: _refreshCount,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// [FeedViewSegment.settle] 的结算结果(feed_viewed 专有属性)。
|
||||
class FeedViewSummary {
|
||||
const FeedViewSummary({
|
||||
required this.durationMs,
|
||||
required this.impressionCount,
|
||||
required this.loadMoreCount,
|
||||
required this.refreshCount,
|
||||
});
|
||||
|
||||
final int durationMs;
|
||||
final int impressionCount;
|
||||
final int loadMoreCount;
|
||||
final int refreshCount;
|
||||
|
||||
/// 结算即上报的便捷口。
|
||||
void report(FeedAnalytics analytics, {FeedTab feedTab = FeedTab.home}) {
|
||||
analytics.feedViewed(
|
||||
feedTab: feedTab,
|
||||
durationMs: durationMs,
|
||||
impressionCount: impressionCount,
|
||||
loadMoreCount: loadMoreCount,
|
||||
refreshCount: refreshCount,
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user